From bd8de10c258e35138d4b30f6efb07d43567fbd11 Mon Sep 17 00:00:00 2001 From: Art Date: Fri, 31 Mar 2023 15:10:51 +0700 Subject: [PATCH 01/32] Add toMatchNamedSnapshot --- CHANGELOG.md | 5 ++ docs/ExpectAPI.md | 10 +++ docs/SnapshotTesting.md | 2 + packages/jest-snapshot/src/index.ts | 97 ++++++++++++++++++++++++++--- packages/jest-snapshot/src/types.ts | 20 ++++++ 5 files changed, 127 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f6424a496c5..fc59dfb95817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,11 @@ ### Performance +## 29.6.0 + +### Features + +- `[jest-snapshot]` Add `toMatchNamedSnapshot` to bring snapshot support to concurrent mode ## 29.5.0 ### Features diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index d140e0dd4340..20543c996d88 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -744,6 +744,16 @@ You can provide an optional `propertyMatchers` object argument, which has asymme 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. +### `.toMatchNamedSnapshot(propertyMatchers?, snapshotName?)` + +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 functions as the test name. By providing a snapshot name (as opposed to letting Jest infer the name from context) snapshot names can be guaranteed to be consistent across test runs, whatever the context at the time of evaluation. Jest always appends a number at the end of a snapshot name. + +Jest sorts snapshots by name in the corresponding `.snap` file. + ### `.toMatchInlineSnapshot(propertyMatchers?, inlineSnapshot)` Ensures that a value matches the most recent snapshot. diff --git a/docs/SnapshotTesting.md b/docs/SnapshotTesting.md index dfbe33c25995..23ae76570326 100644 --- a/docs/SnapshotTesting.md +++ b/docs/SnapshotTesting.md @@ -264,6 +264,8 @@ Now, every time the snapshot test case runs, `Date.now()` will return `148236336 Always strive to use descriptive test and/or snapshot names for snapshots. The best names describe the expected snapshot content. This makes it easier for reviewers to verify the snapshots during review, and for anyone to know whether or not an outdated snapshot is the correct behavior before updating. +The default matcher, `toMatchSnapshot`, infers the name based on the test function context when the snapshot is evaluated. This can cause snapshots in tests that are run concurrently to have different names on each test run. If you want to guarantee consistent names, you can use `toMatchNamedSnapshot` as an alternative matcher. + For example, compare: ```js diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index dd310c0773af..02ae771fff17 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -30,7 +30,12 @@ import { printReceived, printSnapshotAndReceived, } from './printSnapshot'; -import type {Context, FileSystem, MatchSnapshotConfig} from './types'; +import type { + Context, + FileSystem, + MatchSnapshotConfig, + SnapshotNameConfig, +} from './types'; import {deepMerge, escapeBacktickString, serialize} from './utils'; export {addSerializer, getSerializers} from './plugins'; @@ -67,6 +72,19 @@ const printSnapshotName = ( } ${count}\``; }; +const getSnapshotName = (config: SnapshotNameConfig): string => { + const {snapshotName, currentTestName, hint} = config; + + if (snapshotName) { + return snapshotName; + } + if (currentTestName && hint) { + return `${currentTestName}: ${hint}`; + } + // future BREAKING change: || hint + return currentTestName || ''; +}; + function stripAddedIndentation(inlineSnapshot: string) { // Find indentation if exists. const match = inlineSnapshot.match(INDENTATION_REGEX); @@ -209,6 +227,67 @@ export const toMatchSnapshot: MatcherFunctionWithContext< }); }; +export const toMatchNamedSnapshot: MatcherFunctionWithContext< + Context, + [propertiesOrSnapshot?: object | string, snapshotName?: string] +> = function (received, propertiesOrSnapshotName, snapshotName) { + const matcherName = 'toMatchNamedSnapshot'; + let properties; + + const length = arguments.length; + if (length === 2 && typeof propertiesOrSnapshotName === 'string') { + snapshotName = propertiesOrSnapshotName; + } else if (length >= 2) { + if ( + Array.isArray(propertiesOrSnapshotName) || + typeof propertiesOrSnapshotName !== 'object' || + propertiesOrSnapshotName === null + ) { + const options: MatcherHintOptions = { + isNot: this.isNot, + promise: this.promise, + }; + let printedWithType = printWithType( + 'Expected properties', + propertiesOrSnapshotName, + printExpected, + ); + + if (length === 3) { + options.secondArgument = 'snapshotName'; + options.secondArgumentColor = BOLD_WEIGHT; + + if (propertiesOrSnapshotName == null) { + printedWithType += + "\n\nTo provide a snapshot name without properties: toMatchNamedSnapshot('snapshotName')"; + } + } + + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, PROPERTIES_ARG, options), + `Expected ${EXPECTED_COLOR('properties')} must be an object`, + printedWithType, + ), + ); + } + + // Future breaking change: Snapshot hint must be a string + // if (arguments.length === 3 && typeof hint !== 'string') {} + + properties = propertiesOrSnapshotName; + } + + return _toMatchSnapshot({ + context: this, + isInline: false, + matcherName, + properties, + received, + snapshotName, + }); +}; + export const toMatchInlineSnapshot: MatcherFunctionWithContext< Context, [propertiesOrSnapshot?: object | string, inlineSnapshot?: string] @@ -273,8 +352,15 @@ export const toMatchInlineSnapshot: MatcherFunctionWithContext< }; const _toMatchSnapshot = (config: MatchSnapshotConfig) => { - const {context, hint, inlineSnapshot, isInline, matcherName, properties} = - config; + const { + context, + hint, + inlineSnapshot, + isInline, + matcherName, + snapshotName, + properties, + } = config; let {received} = config; context.dontThrow && context.dontThrow(); @@ -301,10 +387,7 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => { ); } - const fullTestName = - currentTestName && hint - ? `${currentTestName}: ${hint}` - : currentTestName || ''; // future BREAKING change: || hint + const fullTestName = getSnapshotName({currentTestName, hint, snapshotName}); if (typeof properties === 'object') { if (typeof received !== 'object' || received === null) { diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index 541c191193a7..2cab7378d9d5 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -27,10 +27,17 @@ export type MatchSnapshotConfig = { inlineSnapshot?: string; isInline: boolean; matcherName: string; + snapshotName?: string; properties?: object; received: any; }; +export type SnapshotNameConfig = { + hint?: string; + snapshotName?: string; + currentTestName?: string; +}; + export type SnapshotData = Record; export interface SnapshotMatchers, T> { @@ -47,6 +54,19 @@ export interface SnapshotMatchers, T> { propertyMatchers: Partial, hint?: string, ): R; + /** + * This ensures that a value matches the most recent snapshot. + * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. + */ + toMatchNamedSnapshot>( + propertyMatchers: Partial, + snapshot?: string, + ): R; + /** + * This ensures that a value matches the most recent snapshot. + * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. + */ + toMatchNamedSnapshot(snapshotName?: string): R; /** * This ensures that a value matches the most recent snapshot with property matchers. * Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically. From c5fe51961c22d826b3418d637feca2b2a670a71b Mon Sep 17 00:00:00 2001 From: Art Date: Fri, 31 Mar 2023 15:18:45 +0700 Subject: [PATCH 02/32] add to match named snapshot to expect --- packages/jest-expect/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/jest-expect/src/index.ts b/packages/jest-expect/src/index.ts index b9c865613e0a..a5324c3804fb 100644 --- a/packages/jest-expect/src/index.ts +++ b/packages/jest-expect/src/index.ts @@ -9,6 +9,7 @@ import {expect} from 'expect'; import { addSerializer, toMatchInlineSnapshot, + toMatchNamedSnapshot, toMatchSnapshot, toThrowErrorMatchingInlineSnapshot, toThrowErrorMatchingSnapshot, @@ -29,6 +30,7 @@ export type {JestExpect} from './types'; function createJestExpect(): JestExpect { expect.extend({ toMatchInlineSnapshot, + toMatchNamedSnapshot, toMatchSnapshot, toThrowErrorMatchingInlineSnapshot, toThrowErrorMatchingSnapshot, From 35bbdeb998ecf7bc46c069383f9a73846500ebc9 Mon Sep 17 00:00:00 2001 From: Art Date: Fri, 31 Mar 2023 15:51:59 +0700 Subject: [PATCH 03/32] add test --- .../__snapshots__/printSnapshot.test.ts.snap | 152 +++++++++ .../src/__tests__/printSnapshot.test.ts | 308 ++++++++++++++++++ 2 files changed, 460 insertions(+) diff --git a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap index 1b1aea2e1561..a74871623f55 100644 --- a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap +++ b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap @@ -1,5 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`change text value 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[`matcher error toMatchInlineSnapshot Expected properties must be an object (non-null) without snapshot 1`] = ` expect(received).toMatchInlineSnapshot(properties) @@ -32,6 +48,67 @@ exports[`matcher error toMatchInlineSnapshot Snapshot matchers cannot be used wi Matcher error: Snapshot matchers cannot be used with not `; +exports[`matcher error toMatchNamedSnapshot Expected properties must be an object (non-null) 1`] = ` +expect(received).toMatchNamedSnapshot(properties) + +Matcher error: Expected properties must be an object + +Expected properties has type: function +Expected properties has value: [Function] +`; + +exports[`matcher error toMatchNamedSnapshot Expected properties must be an object (null) with snapshot name 1`] = ` +expect(received).toMatchNamedSnapshot(properties, snapshotName) + +Matcher error: Expected properties must be an object + +Expected properties has value: null + +To provide a snapshot name without properties: toMatchNamedSnapshot('snapshotName') +`; + +exports[`matcher error toMatchNamedSnapshot Expected properties must be an object (null) without snapshot name 1`] = ` +expect(received).toMatchNamedSnapshot(properties) + +Matcher error: Expected properties must be an object + +Expected properties has value: null +`; + +exports[`matcher error toMatchNamedSnapshot Snapshot state must be initialized 1`] = ` +expect(received).resolves.toMatchNamedSnapshot() + +Snapshot state must be initialized + +Snapshot state has value: undefined +`; + +exports[`matcher error toMatchNamedSnapshot received value must be an object (non-null) 1`] = ` +expect(received).toMatchNamedSnapshot(properties) + +Matcher error: received value must be an object when the matcher has properties + +Received has type: string +Received has value: "string" +`; + +exports[`matcher error toMatchNamedSnapshot received value must be an object (null) 1`] = ` +expect(received).toMatchNamedSnapshot(properties) + +Matcher error: received value must be an object when the matcher has properties + +Received has value: null +`; + +exports[`matcher error toMatchSnapshot Expected properties must be an object (array) 1`] = ` +expect(received).toMatchSnapshot(properties) + +Matcher error: Expected properties must be an object + +Expected properties has type: array +Expected properties has value: [] +`; + exports[`matcher error toMatchSnapshot Expected properties must be an object (non-null) 1`] = ` expect(received).toMatchSnapshot(properties) @@ -166,6 +243,32 @@ Snapshot name: \`with properties 1\` } `; +exports[`pass false toMatchNamedSnapshot empty snapshot name New snapshot was not written (multi line) 1`] = ` +expect(received).toMatchNamedSnapshot() + +Snapshot name: \`New snapshot was not written 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 toMatchNamedSnapshot empty snapshot name New snapshot was not written (single line) 1`] = ` +expect(received).toMatchNamedSnapshot() + +Snapshot name: \`New snapshot was not written 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 New snapshot was not written (multi line) 1`] = ` expect(received).toMatchSnapshot(hint) @@ -638,3 +741,52 @@ exports[`printSnapshotAndReceived without serialize prettier/pull/5590 1`] = ` ================================================================================ `; + +exports[`specific-line-diff-false 1`] = ` +expect(received).toMatchNamedSnapshot(properties) + +Snapshot name: \`with properties 1\` + +Expected properties: {"name": "Error"} +Received value: [RangeError: Invalid array length] +`; + +exports[`specific-line-diff-true 1`] = ` +expect(received).toMatchNamedSnapshot(properties) + +Snapshot name: \`with properties 1\` + +- Expected properties - 1 ++ Received value + 1 + + Object { +- "id": "abcdef", ++ "id": "abcdefg", + } +`; + +exports[`specific-not-written-multi-line 1`] = ` +expect(received).toMatchNamedSnapshot() + +Snapshot name: \`New snapshot was not written 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[`specific-not-written-single-line 1`] = ` +expect(received).toMatchNamedSnapshot() + +Snapshot name: \`New snapshot was not written 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!" +`; diff --git a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts index 43edcd13029b..f1e27227fe3b 100644 --- a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts +++ b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts @@ -13,6 +13,7 @@ import format from 'pretty-format'; import { Context, toMatchInlineSnapshot, + toMatchNamedSnapshot, toMatchSnapshot, toThrowErrorMatchingInlineSnapshot, toThrowErrorMatchingSnapshot, @@ -274,6 +275,99 @@ describe('matcher error', () => { }); }); + describe('toMatchNamedSnapshot', () => { + 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: '', + } as Context; + const properties = () => {}; + + expect(() => { + toMatchNamedSnapshot.call(context, received, properties); + }).toThrowErrorMatchingSnapshot(); + }); + + test('Expected properties must be an object (null) with snapshot name', () => { + const context = { + isNot: false, + promise: '', + } as Context; + const properties = null; + const snapshotName = 'obj-snapshot'; + + expect(() => { + // @ts-expect-error: Testing runtime error + toMatchNamedSnapshot.call(context, received, properties, snapshotName); + }).toThrowErrorMatchingSnapshot(); + }); + + test('Expected properties must be an object (null) without snapshot name', () => { + const context = { + isNot: false, + promise: '', + } as Context; + const properties = null; + + expect(() => { + // @ts-expect-error: Testing runtime error + toMatchNamedSnapshot.call(context, received, properties); + }).toThrowErrorMatchingSnapshot(); + }); + + test('Expected properties must be an object (array)', () => { + const context = { + isNot: false, + promise: '', + } as Context; + const properties: Array = []; + + expect(() => { + toMatchNamedSnapshot.call(context, received, properties); + }).toThrowErrorMatchingSnapshot(); + }); + + describe('received value must be an object', () => { + const context = { + currentTestName: '', + isNot: false, + promise: '', + snapshotState: {}, + } as Context; + const properties = {}; + + test('(non-null)', () => { + expect(() => { + toMatchNamedSnapshot.call(context, 'string', properties); + }).toThrowErrorMatchingSnapshot(); + }); + + test('(null)', () => { + expect(() => { + toMatchNamedSnapshot.call(context, null, properties); + }).toThrowErrorMatchingSnapshot(); + }); + }); + + test('Snapshot state must be initialized', () => { + const context = { + isNot: false, + promise: 'resolves', + } as Context; + const snapshotName = 'initialize me'; + + expect(() => { + toMatchNamedSnapshot.call(context, received, snapshotName); + }).toThrowErrorMatchingSnapshot(); + }); + }); + describe('toMatchSnapshot', () => { const received = { id: 'abcdef', @@ -554,6 +648,220 @@ describe('pass false', () => { }); }); + describe('toMatchNamedSnapshot', () => { + describe('empty snapshot name', () => { + 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, + }; + }, + }, + } as Context; + const received = 'To write or not to write,\nthat is the question.'; + const snapshotName = ''; + + const {message, pass} = toMatchNamedSnapshot.call( + context, + received, + snapshotName, + ) as SyncExpectationResult; + expect(pass).toBe(false); + expect(message()).toMatchNamedSnapshot(snapshotName); + }); + + 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, + }; + }, + }, + } as Context; + const received = 'Write me if you can!'; + const snapshotName = ''; + + const {message, pass} = toMatchNamedSnapshot.call( + context, + received, + snapshotName, + ) as SyncExpectationResult; + expect(pass).toBe(false); + expect(message()).toMatchNamedSnapshot(snapshotName); + }); + }); + + describe('specific snapshot name', () => { + 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, + }; + }, + }, + } as Context; + const received = 'To write or not to write,\nthat is the question.'; + const snapshotName = 'specific-not-written-multi-line'; + + const {message, pass} = toMatchNamedSnapshot.call( + context, + received, + snapshotName, + ) as SyncExpectationResult; + expect(pass).toBe(false); + expect(message()).toMatchNamedSnapshot(snapshotName); + }); + + 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, + }; + }, + }, + } as Context; + const received = 'Write me if you can!'; + const snapshotName = 'specific-not-written-single-line'; + + const {message, pass} = toMatchNamedSnapshot.call( + context, + received, + snapshotName, + ) as SyncExpectationResult; + expect(pass).toBe(false); + expect(message()).toMatchNamedSnapshot(snapshotName); + }); + }); + + 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: string) => `${fullTestName} 1`, + }, + utils: { + iterableEquality: () => {}, + subsetEquality: () => {}, + }, + } as unknown as Context; + + test('isLineDiffable false', () => { + const snapshotName = 'specific-line-diff-false'; + + const {message, pass} = toMatchNamedSnapshot.call( + context, + new RangeError('Invalid array length'), + {name: 'Error'}, + snapshotName, + ) as SyncExpectationResult; + expect(pass).toBe(false); + expect(message()).toMatchNamedSnapshot(snapshotName); + }); + + test('isLineDiffable true', () => { + const snapshotName = 'specific-line-diff-true'; + const received = { + id: 'abcdefg', + text: 'Increase code coverage', + type, + }; + + const {message, pass} = toMatchNamedSnapshot.call( + context, + received, + properties, + snapshotName, + ) as SyncExpectationResult; + expect(pass).toBe(false); + expect(message()).toMatchNamedSnapshot(snapshotName); + }); + }); + + 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, + }; + }, + } as SnapshotState, + utils: { + iterableEquality: () => {}, + subsetEquality: () => {}, + }, + } as unknown as Context; + const received = { + id, + text: 'received', + type, + }; + const snapshotName = 'change text value'; + + const {message, pass} = toMatchSnapshot.call( + context, + received, + properties, + snapshotName, + ) as SyncExpectationResult; + expect(pass).toBe(false); + expect(message()).toMatchNamedSnapshot(snapshotName); + }); + }); + }); + describe('toMatchSnapshot', () => { test('New snapshot was not written (multi line)', () => { const context = { From 6841acf328d7690c99adc1353c1fb725e46a42d2 Mon Sep 17 00:00:00 2001 From: Art Date: Mon, 3 Apr 2023 18:34:20 +0700 Subject: [PATCH 04/32] add e2e testing --- e2e/__tests__/toMatchNamedSnapshot.test.ts | 291 +++++++++++++++++++++ e2e/__tests__/toMatchSnapshot.test.ts | 6 +- e2e/to-match-named-snapshot/package.json | 5 + 3 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 e2e/__tests__/toMatchNamedSnapshot.test.ts create mode 100644 e2e/to-match-named-snapshot/package.json diff --git a/e2e/__tests__/toMatchNamedSnapshot.test.ts b/e2e/__tests__/toMatchNamedSnapshot.test.ts new file mode 100644 index 000000000000..906a295bd5be --- /dev/null +++ b/e2e/__tests__/toMatchNamedSnapshot.test.ts @@ -0,0 +1,291 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as path from 'path'; +import {cleanup, makeTemplate, writeFiles} from '../Utils'; +import runJest from '../runJest'; + +const DIR = path.resolve(__dirname, '../to-match-named-snapshot'); +const TESTS_DIR = path.resolve(DIR, '__tests__'); + +beforeEach(() => cleanup(TESTS_DIR)); +afterAll(() => cleanup(TESTS_DIR)); + +test('basic support', () => { + const filename = 'basic-support.test.js'; + const template = makeTemplate( + "test('named snapshots', () => expect($1).toMatchNamedSnapshot('basic-support'));", + ); + + { + writeFiles(TESTS_DIR, { + [filename]: template(['{apple: "original value"}']), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + } + + { + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Snapshots: 1 passed, 1 total'); + expect(stderr).not.toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + } + + { + writeFiles(TESTS_DIR, { + [filename]: template(['{apple: "updated value"}']), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Snapshot name: `named snapshots 1`'); + expect(exitCode).toBe(1); + } + + { + const {stderr, exitCode} = runJest(DIR, [ + '-w=1', + '--ci=false', + filename, + '-u', + ]); + expect(stderr).toMatch('1 snapshot updated from 1 test suite.'); + expect(exitCode).toBe(0); + } +}); + +test('error thrown before snapshot', () => { + const filename = 'error-thrown-before-snapshot.test.js'; + const template = makeTemplate(`test('snapshots', () => { + expect($1).toBeTruthy(); + expect($2).toMatchNamedSnapshot('error-thrown-before-snapshot-$3'); + });`); + + { + writeFiles(TESTS_DIR, { + [filename]: template(['true', '{a: "original"}', '1']), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + } + + { + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Snapshots: 1 passed, 1 total'); + expect(exitCode).toBe(0); + } + + { + writeFiles(TESTS_DIR, { + [filename]: template(['false', '{a: "original"}', '2']), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).not.toMatch('1 obsolete snapshot found'); + expect(exitCode).toBe(1); + } +}); + +test('first snapshot fails, second passes', () => { + const filename = 'first-snapshot-fails-second-passes.test.js'; + const template = makeTemplate(`test('named snapshots', () => { + expect($1).toMatchNamedSnapshot('test-snapshot'); + expect($2).toMatchNamedSnapshot('test-snapshot'); + });`); + + { + writeFiles(TESTS_DIR, {[filename]: template(["'apple'", "'banana'", '1'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('2 snapshots written from 1 test suite.'); + expect(exitCode).toBe(0); + } + + { + writeFiles(TESTS_DIR, {[filename]: template(["'kiwi'", "'banana'", '2'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Snapshot name: `named snapshots 1`'); + // Match lines separately because empty line has been replaced with space: + expect(stderr).toMatch('Snapshot: "apple"'); + expect(stderr).toMatch('Received: "kiwi"'); + expect(stderr).not.toMatch('1 obsolete snapshot found'); + expect(exitCode).toBe(1); + } +}); + +test('does not mark snapshots as obsolete in skipped tests if snapshot name match test name', () => { + const filename = 'no-obsolete-if-skipped.test.js'; + const template = makeTemplate(`test('snapshots', () => { + expect(true).toBe(true); + }); + + $1('will be skipped', () => { + expect({a: 6}).toMatchNamedSnapshot('will be skipped'); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template(['test'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + } + + { + writeFiles(TESTS_DIR, {[filename]: template(['test.skip'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + console.log(stderr); + expect(stderr).not.toMatch('1 obsolete snapshot found'); + expect(exitCode).toBe(0); + } +}); + +test('mark snapshots as obsolete in skipped tests if snapshot name does not match test name', () => { + const filename = 'no-obsolete-if-skipped.test.js'; + const template = makeTemplate(`test('snapshots', () => { + expect(true).toBe(true); + }); + + $1('will be skipped', () => { + expect({a: 6}).toMatchNamedSnapshot('will be obsolete'); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template(['test'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + } + + { + writeFiles(TESTS_DIR, {[filename]: template(['test.skip'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + console.log(stderr); + expect(stderr).toMatch('1 snapshot obsolete'); + expect(exitCode).toBe(1); + } +}); + +test('accepts custom snapshot name', () => { + const filename = 'accept-custom-snapshot-name.test.js'; + const template = makeTemplate(`test('accepts custom snapshot name', () => { + expect(true).toMatchNamedSnapshot('custom-name'); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + } +}); + +test('handles property matchers', () => { + const filename = 'handle-property-matchers.test.js'; + const template = makeTemplate(`test('handles property matchers', () => { + expect({createdAt: $1}).toMatchNamedSnapshot({createdAt: expect.any(Date)}, 'property-matchers'); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template(['new Date()'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + } + + { + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Snapshots: 1 passed, 1 total'); + expect(exitCode).toBe(0); + } + + { + writeFiles(TESTS_DIR, {[filename]: template(['"string"'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Snapshot name: `handles property matchers 1`'); + expect(stderr).toMatch('Snapshots: 1 failed, 1 total'); + expect(exitCode).toBe(1); + } +}); + +test('handles invalid property matchers', () => { + const filename = 'handle-property-matchers.test.js'; + { + writeFiles(TESTS_DIR, { + [filename]: `test('invalid property matchers', () => { + expect({foo: 'bar'}).toMatchNamedSnapshot(null, 'null-property-matcher'); + }); + `, + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Expected properties must be an object'); + expect(exitCode).toBe(1); + } + { + writeFiles(TESTS_DIR, { + [filename]: `test('invalid property matchers', () => { + expect({foo: 'bar'}).toMatchNamedSnapshot(undefined, 'undefined-property-matcher'); + }); + `, + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Expected properties must be an object'); + expect(stderr).toMatch( + "To provide a snapshot name without properties: toMatchNamedSnapshot('snapshotName')", + ); + expect(exitCode).toBe(1); + } +}); + +test('handles property matchers with deep properties', () => { + const filename = 'handle-property-matchers-with-name.test.js'; + const template = + makeTemplate(`test('handles property matchers with deep properties', () => { + expect({ user: { createdAt: $1, name: $2 }}).toMatchNamedSnapshot({ user: { createdAt: expect.any(Date), name: $2 }}, 'deep-property-matchers'); + }); + `); + + { + writeFiles(TESTS_DIR, { + [filename]: template(['new Date()', '"Jest"']), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + } + + { + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Snapshots: 1 passed, 1 total'); + expect(exitCode).toBe(0); + } + + { + writeFiles(TESTS_DIR, {[filename]: template(['"string"', '"Jest"'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch( + 'Snapshot name: `handles property matchers with deep properties 1`', + ); + expect(stderr).toMatch('Expected properties'); + expect(stderr).toMatch('Snapshots: 1 failed, 1 total'); + expect(exitCode).toBe(1); + } + + { + writeFiles(TESTS_DIR, { + [filename]: template(['new Date()', '"CHANGED"']), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch( + 'Snapshot name: `handles property matchers with deep properties 1`', + ); + expect(stderr).toMatch('Snapshots: 1 failed, 1 total'); + expect(exitCode).toBe(1); + } +}); diff --git a/e2e/__tests__/toMatchSnapshot.test.ts b/e2e/__tests__/toMatchSnapshot.test.ts index c31bfaf4d623..7edc3c43cf2b 100644 --- a/e2e/__tests__/toMatchSnapshot.test.ts +++ b/e2e/__tests__/toMatchSnapshot.test.ts @@ -142,10 +142,10 @@ test('does not mark snapshots as obsolete in skipped tests', () => { } }); -test('accepts custom snapshot name', () => { +test('accepts snapshot hint', () => { const filename = 'accept-custom-snapshot-name.test.js'; - const template = makeTemplate(`test('accepts custom snapshot name', () => { - expect(true).toMatchSnapshot('custom-name'); + const template = makeTemplate(`test('accepts snapshot hint', () => { + expect(true).toMatchSnapshot('custom-hint'); }); `); diff --git a/e2e/to-match-named-snapshot/package.json b/e2e/to-match-named-snapshot/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/to-match-named-snapshot/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} From 7dfe361ed1a0a08114ba66ac1dcae888fa432bf4 Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 4 Apr 2023 14:18:20 +0700 Subject: [PATCH 05/32] add to throw error matching named snapshot with e2e --- .../toThrowErrorMatchingNamedSnapshot.test.ts | 109 ++++++++++++++++++ .../toThrowErrorMatchingSnapshot.test.ts | 5 +- .../package.json | 5 + .../package.json | 5 + packages/expect/src/index.ts | 3 +- packages/jest-expect/src/index.ts | 2 + packages/jest-snapshot/src/index.ts | 30 ++++- packages/jest-snapshot/src/types.ts | 12 +- 8 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 e2e/__tests__/toThrowErrorMatchingNamedSnapshot.test.ts create mode 100644 e2e/to-match-named-snapshot-with-retries/package.json create mode 100644 e2e/to-throw-error-matching-named-snapshot/package.json diff --git a/e2e/__tests__/toThrowErrorMatchingNamedSnapshot.test.ts b/e2e/__tests__/toThrowErrorMatchingNamedSnapshot.test.ts new file mode 100644 index 000000000000..d3ea510df58b --- /dev/null +++ b/e2e/__tests__/toThrowErrorMatchingNamedSnapshot.test.ts @@ -0,0 +1,109 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as path from 'path'; +import * as fs from 'graceful-fs'; +import {cleanup, makeTemplate, writeFiles} from '../Utils'; +import runJest from '../runJest'; + +const DIR = path.resolve( + __dirname, + '../to-throw-error-matching-named-snapshot', +); +const TESTS_DIR = path.resolve(DIR, '__tests__'); + +beforeEach(() => cleanup(TESTS_DIR)); +afterAll(() => cleanup(TESTS_DIR)); + +test('works fine when function throws error', () => { + const filename = 'works-fine-when-function-throws-error.test.js'; + const template = + makeTemplate(`test('works fine when function throws error', () => { + expect(() => { throw new Error('apple'); }) + .toThrowErrorMatchingNamedSnapshot('works-fine'); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + } +}); + +test("throws the error if tested function didn't throw error", () => { + const filename = 'throws-if-tested-function-did-not-throw.test.js'; + const template = + makeTemplate(`test('throws the error if tested function did not throw error', () => { + expect(() => {}).toThrowErrorMatchingNamedSnapshot('error if did not throw error'); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Received function did not throw'); + expect(exitCode).toBe(1); + } +}); + +test('accepts custom snapshot name', () => { + const filename = 'accept-custom-snapshot-name.test.js'; + const template = makeTemplate(`test('accepts custom snapshot name', () => { + expect(() => { throw new Error('apple'); }) + .toThrowErrorMatchingNamedSnapshot('custom-name'); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + } +}); + +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(() => { throw new Error('apple'); }) + .not + .toThrowErrorMatchingNamedSnapshot('cannot-used-with-not'); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Snapshot matchers cannot be used with not'); + expect(exitCode).toBe(1); + } +}); + +test('should support rejecting promises', () => { + const filename = 'should-support-rejecting-promises.test.js'; + const template = + makeTemplate(`test('should support rejecting promises', () => { + return expect(Promise.reject(new Error('octopus'))).rejects.toThrowErrorMatchingNamedSnapshot('support-reject'); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + + const snapshot = fs.readFileSync( + `${TESTS_DIR}/__snapshots__/${filename}.snap`, + 'utf8', + ); + + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(snapshot).toMatchNamedSnapshot('support-reject'); + expect(exitCode).toBe(0); + } +}); diff --git a/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts b/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts index 0137be8037a5..e02e394919a6 100644 --- a/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts +++ b/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts @@ -49,11 +49,11 @@ test("throws the error if tested function didn't throw error", () => { } }); -test('accepts custom snapshot name', () => { +test('accepts custom snapshot hint', () => { const filename = 'accept-custom-snapshot-name.test.js'; const template = makeTemplate(`test('accepts custom snapshot name', () => { expect(() => { throw new Error('apple'); }) - .toThrowErrorMatchingSnapshot('custom-name'); + .toThrowErrorMatchingSnapshot('custom-hint'); }); `); @@ -93,6 +93,7 @@ test('should support rejecting promises', () => { { writeFiles(TESTS_DIR, {[filename]: template()}); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + console.log(stderr); const snapshot = fs.readFileSync( `${TESTS_DIR}/__snapshots__/${filename}.snap`, diff --git a/e2e/to-match-named-snapshot-with-retries/package.json b/e2e/to-match-named-snapshot-with-retries/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/to-match-named-snapshot-with-retries/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/e2e/to-throw-error-matching-named-snapshot/package.json b/e2e/to-throw-error-matching-named-snapshot/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/to-throw-error-matching-named-snapshot/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index 5cc2cd68f6c3..eda221ff209b 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -91,7 +91,8 @@ const getPromiseMatcher = (name: string, matcher: RawMatcherFn) => { return createThrowMatcher(name, true); } else if ( name === 'toThrowErrorMatchingSnapshot' || - name === 'toThrowErrorMatchingInlineSnapshot' + name === 'toThrowErrorMatchingInlineSnapshot' || + name === 'toThrowErrorMatchingNamedSnapshot' ) { return createToThrowErrorMatchingSnapshotMatcher(matcher); } diff --git a/packages/jest-expect/src/index.ts b/packages/jest-expect/src/index.ts index a5324c3804fb..bac4d4c5c37d 100644 --- a/packages/jest-expect/src/index.ts +++ b/packages/jest-expect/src/index.ts @@ -12,6 +12,7 @@ import { toMatchNamedSnapshot, toMatchSnapshot, toThrowErrorMatchingInlineSnapshot, + toThrowErrorMatchingNamedSnapshot, toThrowErrorMatchingSnapshot, } from 'jest-snapshot'; import type {JestExpect} from './types'; @@ -33,6 +34,7 @@ function createJestExpect(): JestExpect { toMatchNamedSnapshot, toMatchSnapshot, toThrowErrorMatchingInlineSnapshot, + toThrowErrorMatchingNamedSnapshot, toThrowErrorMatchingSnapshot, }); diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index 02ae771fff17..08932443ad5f 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -546,12 +546,37 @@ export const toThrowErrorMatchingInlineSnapshot: MatcherFunctionWithContext< ); }; +export const toThrowErrorMatchingNamedSnapshot: MatcherFunctionWithContext< + Context, + [snapshotName?: string, fromPromise?: boolean] +> = function (received, snapshotName, fromPromise) { + const matcherName = 'toThrowErrorMatchingNamedSnapshot'; + + return _toThrowErrorMatchingSnapshot( + { + context: this, + isInline: false, + matcherName, + received, + snapshotName, + }, + fromPromise, + ); +}; + const _toThrowErrorMatchingSnapshot = ( config: MatchSnapshotConfig, fromPromise?: boolean, ) => { - const {context, hint, inlineSnapshot, isInline, matcherName, received} = - config; + const { + context, + hint, + inlineSnapshot, + isInline, + matcherName, + snapshotName, + received, + } = config; context.dontThrow && context.dontThrow(); @@ -606,5 +631,6 @@ const _toThrowErrorMatchingSnapshot = ( isInline, matcherName, received: error.message, + snapshotName, }); }; diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index 2cab7378d9d5..69ccae322464 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -55,7 +55,8 @@ export interface SnapshotMatchers, T> { hint?: string, ): R; /** - * This ensures that a value matches the most recent snapshot. + * This ensures that a value matches the specific snapshot. + * Instead of use current test name in global state, it will use the specific name to find the snapshot. * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. */ toMatchNamedSnapshot>( @@ -63,7 +64,8 @@ export interface SnapshotMatchers, T> { snapshot?: string, ): R; /** - * This ensures that a value matches the most recent snapshot. + * This ensures that a value matches the specific snapshot. + * Instead of use current test name in global state, it will use the specific name to find the snapshot. * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. */ toMatchNamedSnapshot(snapshotName?: string): R; @@ -91,6 +93,12 @@ export interface SnapshotMatchers, T> { * Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically. */ toThrowErrorMatchingInlineSnapshot(snapshot?: string): R; + + /** + * Used to test that a function throws a error matching the specific snapshot. + * Instead of use current test name in global state, it will use the specific name to find the snapshot. + */ + toThrowErrorMatchingNamedSnapshot(name?: string): R; } export type SnapshotFormat = Omit; From b684d88b9a7eb7ec1df17f476cb67ffe03489af8 Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 4 Apr 2023 14:57:44 +0700 Subject: [PATCH 06/32] add unit test named snapshot --- .../__snapshots__/printSnapshot.test.ts.snap | 15 +++ .../src/__tests__/printSnapshot.test.ts | 40 ++++++ .../src/__tests__/throwMatcher.test.ts | 118 +++++++++++++----- 3 files changed, 142 insertions(+), 31 deletions(-) diff --git a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap index a74871623f55..5c4d0c34673e 100644 --- a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap +++ b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap @@ -178,6 +178,21 @@ Snapshot state must be initialized Snapshot state has value: undefined `; +exports[`matcher error toThrowErrorMatchingNamedSnapshot Received value must be a function 1`] = ` +expect(received).toThrowErrorMatchingNamedSnapshot() + +Matcher error: received value must be a function + +Received has type: number +Received has value: 13 +`; + +exports[`matcher error toThrowErrorMatchingNamedSnapshot Snapshot matchers cannot be used with not 1`] = ` +expect(received).not.toThrowErrorMatchingNamedSnapshot() + +Matcher error: Snapshot matchers cannot be used with not +`; + exports[`matcher error toThrowErrorMatchingSnapshot Received value must be a function 1`] = ` expect(received).toThrowErrorMatchingSnapshot() diff --git a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts index f1e27227fe3b..0206d0b2fbae 100644 --- a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts +++ b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts @@ -16,6 +16,7 @@ import { toMatchNamedSnapshot, toMatchSnapshot, toThrowErrorMatchingInlineSnapshot, + toThrowErrorMatchingNamedSnapshot, toThrowErrorMatchingSnapshot, } from '../'; import type SnapshotState from '../State'; @@ -529,6 +530,45 @@ describe('matcher error', () => { // Future test: Snapshot hint must be a string }); + + describe('toThrowErrorMatchingNamedSnapshot', () => { + test('Received value must be a function', () => { + const context = { + isNot: false, + promise: '', + } as Context; + const received = 13; + const fromPromise = false; + + expect(() => { + toThrowErrorMatchingNamedSnapshot.call( + context, + received, + '', + fromPromise, + ); + }).toThrowErrorMatchingSnapshot(); + }); + + test('Snapshot matchers cannot be used with not', () => { + const snapshotName = 'to-throw-snapshot'; + const context = { + isNot: true, + promise: '', + } as Context; + const received = new Error('received'); + const fromPromise = true; + + expect(() => { + toThrowErrorMatchingNamedSnapshot.call( + context, + received, + snapshotName, + fromPromise, + ); + }).toThrowErrorMatchingSnapshot(); + }); + }); }); describe('other error', () => { diff --git a/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts b/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts index 9293e8e24762..b7b35a60073c 100644 --- a/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts +++ b/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts @@ -5,7 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -import {type Context, toThrowErrorMatchingSnapshot} from '../'; +import { + Context, + toThrowErrorMatchingNamedSnapshot, + toThrowErrorMatchingSnapshot, +} from '../'; const mockedMatch = jest.fn(() => ({ actual: 'coconut', @@ -20,50 +24,102 @@ afterEach(() => { jest.clearAllMocks(); }); -it('throw matcher can take func', () => { - toThrowErrorMatchingSnapshot.call( - mockedContext, - () => { - throw new Error('coconut'); - }, - undefined, - false, - ); - - expect(mockedMatch).toHaveBeenCalledTimes(1); - expect(mockedMatch).toHaveBeenCalledWith( - expect.objectContaining({received: 'coconut', testName: ''}), - ); -}); - -describe('throw matcher from promise', () => { - it('can take error', () => { +describe('throw matcher can take func', () => { + it('toThrowErrorMatchingSnapshot', () => { toThrowErrorMatchingSnapshot.call( mockedContext, - new Error('coco'), - 'testName', - true, + () => { + throw new Error('coconut'); + }, + undefined, + false, ); expect(mockedMatch).toHaveBeenCalledTimes(1); expect(mockedMatch).toHaveBeenCalledWith( - expect.objectContaining({received: 'coco', testName: ''}), + expect.objectContaining({received: 'coconut', testName: ''}), ); }); - it('can take custom error', () => { - class CustomError extends Error {} - - toThrowErrorMatchingSnapshot.call( + it('toThrowErrorMatchingNamedSnapshot', () => { + toThrowErrorMatchingNamedSnapshot.call( mockedContext, - new CustomError('nut'), - 'testName', - true, + () => { + throw new Error('coconut'); + }, + undefined, + false, ); expect(mockedMatch).toHaveBeenCalledTimes(1); expect(mockedMatch).toHaveBeenCalledWith( - expect.objectContaining({received: 'nut', testName: ''}), + expect.objectContaining({received: 'coconut', testName: ''}), ); }); }); + +describe('throw matcher from promise', () => { + describe('toThrowErrorMatchingSnapshot', () => { + it('can take error', () => { + toThrowErrorMatchingSnapshot.call( + mockedContext, + new Error('coco'), + 'testName', + true, + ); + + expect(mockedMatch).toHaveBeenCalledTimes(1); + expect(mockedMatch).toHaveBeenCalledWith( + expect.objectContaining({received: 'coco', testName: ''}), + ); + }); + + it('can take custom error', () => { + class CustomError extends Error {} + + toThrowErrorMatchingSnapshot.call( + mockedContext, + new CustomError('nut'), + 'testName', + true, + ); + + expect(mockedMatch).toHaveBeenCalledTimes(1); + expect(mockedMatch).toHaveBeenCalledWith( + expect.objectContaining({received: 'nut', testName: ''}), + ); + }); + }); + + describe('toThrowErrorMatchingNamedSnapshot', () => { + it('can take error', () => { + toThrowErrorMatchingNamedSnapshot.call( + mockedContext, + new Error('coco'), + 'snapshot name', + true, + ); + + expect(mockedMatch).toHaveBeenCalledTimes(1); + expect(mockedMatch).toHaveBeenCalledWith( + expect.objectContaining({received: 'coco', testName: 'snapshot name'}), + ); + }); + + it('can take custom error', () => { + class CustomError extends Error {} + + toThrowErrorMatchingNamedSnapshot.call( + mockedContext, + new CustomError('nut'), + 'snapshot name', + true, + ); + + expect(mockedMatch).toHaveBeenCalledTimes(1); + expect(mockedMatch).toHaveBeenCalledWith( + expect.objectContaining({received: 'nut', testName: 'snapshot name'}), + ); + }); + }); +}); From 9445a2f4f8705ebef7e162a535ead0ca45afceb0 Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 4 Apr 2023 14:58:06 +0700 Subject: [PATCH 07/32] add e2e toMatchNamedSnapshot with retries --- .../toMatchNamedSnapshotWithRetries.test.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 e2e/__tests__/toMatchNamedSnapshotWithRetries.test.ts diff --git a/e2e/__tests__/toMatchNamedSnapshotWithRetries.test.ts b/e2e/__tests__/toMatchNamedSnapshotWithRetries.test.ts new file mode 100644 index 000000000000..b5557f614825 --- /dev/null +++ b/e2e/__tests__/toMatchNamedSnapshotWithRetries.test.ts @@ -0,0 +1,102 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as path from 'path'; +import {skipSuiteOnJasmine} from '@jest/test-utils'; +import {cleanup, makeTemplate, writeFiles} from '../Utils'; +import runJest from '../runJest'; + +const DIR = path.resolve(__dirname, '../to-match-named-snapshot-with-retries'); +const TESTS_DIR = path.resolve(DIR, '__tests__'); + +beforeEach(() => cleanup(TESTS_DIR)); +afterAll(() => cleanup(TESTS_DIR)); + +skipSuiteOnJasmine(); + +test('works with a single snapshot', () => { + const filename = 'basic-support.test.js'; + const template = makeTemplate(` + let index = 0; + afterEach(() => { + index += 1; + }); + jest.retryTimes($2); + test('snapshots', () => expect($1).toMatchNamedSnapshot('snapshot-retries')); + `); + + { + writeFiles(TESTS_DIR, { + [filename]: template(['3', '1' /* retries */]), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + } + + { + writeFiles(TESTS_DIR, { + [filename]: template(['index', '2' /* retries */]), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Received: 2'); + expect(stderr).toMatch('1 snapshot failed from 1 test suite.'); + expect(exitCode).toBe(1); + } + + { + writeFiles(TESTS_DIR, { + [filename]: template(['index', '4' /* retries */]), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Snapshots: 1 passed, 1 total'); + expect(exitCode).toBe(0); + } +}); + +test('works when multiple tests have snapshots but only one of them failed multiple times', () => { + const filename = 'basic-support.test.js'; + const template = makeTemplate(` + test('passing snapshots', () => expect('foo').toMatchNamedSnapshot()); + describe('with retries', () => { + let index = 0; + afterEach(() => { + index += 1; + }); + jest.retryTimes($2); + test('snapshots', () => expect($1).toMatchNamedSnapshot()); + }); + `); + + { + writeFiles(TESTS_DIR, { + [filename]: template(['3', '2' /* retries */]), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('2 snapshots written from 1 test suite.'); + expect(exitCode).toBe(0); + } + + { + writeFiles(TESTS_DIR, { + [filename]: template(['index', '2' /* retries */]), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Received: 2'); + expect(stderr).toMatch('1 snapshot failed from 1 test suite.'); + expect(exitCode).toBe(1); + } + + { + writeFiles(TESTS_DIR, { + [filename]: template(['index', '4' /* retries */]), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Snapshots: 1 passed, 1 total'); + expect(exitCode).toBe(0); + } +}); From 6fb5b85c3ef8ce5451e6af9af1fde4fc220d5806 Mon Sep 17 00:00:00 2001 From: Art Date: Wed, 5 Apr 2023 19:07:26 +0700 Subject: [PATCH 08/32] add snapshot file --- .../toThrowErrorMatchingNamedSnapshot.test.ts.snap | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 e2e/__tests__/__snapshots__/toThrowErrorMatchingNamedSnapshot.test.ts.snap diff --git a/e2e/__tests__/__snapshots__/toThrowErrorMatchingNamedSnapshot.test.ts.snap b/e2e/__tests__/__snapshots__/toThrowErrorMatchingNamedSnapshot.test.ts.snap new file mode 100644 index 000000000000..4f9e826c3398 --- /dev/null +++ b/e2e/__tests__/__snapshots__/toThrowErrorMatchingNamedSnapshot.test.ts.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`support-reject 1`] = ` +"// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[\`support-reject 1\`] = \`"octopus"\`; +" +`; From f28f669617cc1496377a330bbd75e70953b39ab5 Mon Sep 17 00:00:00 2001 From: Art Date: Wed, 5 Apr 2023 19:07:39 +0700 Subject: [PATCH 09/32] add types test --- .../__typetests__/matchers.test.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/packages/jest-snapshot/__typetests__/matchers.test.ts b/packages/jest-snapshot/__typetests__/matchers.test.ts index 8e4dd53dcb87..88a5a8164c92 100644 --- a/packages/jest-snapshot/__typetests__/matchers.test.ts +++ b/packages/jest-snapshot/__typetests__/matchers.test.ts @@ -11,8 +11,10 @@ import { Context, SnapshotState, toMatchInlineSnapshot, + toMatchNamedSnapshot, toMatchSnapshot, toThrowErrorMatchingInlineSnapshot, + toThrowErrorMatchingNamedSnapshot, toThrowErrorMatchingSnapshot, } from 'jest-snapshot'; @@ -78,6 +80,43 @@ expectType( expectError(toMatchInlineSnapshot({received: 'value'})); +// toMatchNamedSnapshot + +expectType( + toMatchNamedSnapshot.call({} as Context, {received: 'value'}), +); + +expectType( + toMatchNamedSnapshot.call({} as Context, {received: 'value'}), +); + +expectType( + toMatchNamedSnapshot.call( + {} as Context, + {received: 'value'}, + 'snapshot name', + ), +); + +expectType( + toMatchNamedSnapshot.call( + {} as Context, + {received: 'value'}, + {property: 'match'}, + ), +); + +expectType( + toMatchNamedSnapshot.call( + {} as Context, + {received: 'value'}, + {property: 'match'}, + 'snapshot name', + ), +); + +expectError(toMatchNamedSnapshot({received: 'value'})); + // toThrowErrorMatchingSnapshot expectType( @@ -145,3 +184,37 @@ expectType( ); expectError(toThrowErrorMatchingInlineSnapshot({received: 'value'})); + +// toThrowErrorMatchingNamedSnapshot + +expectType( + toThrowErrorMatchingNamedSnapshot.call({} as Context, new Error('received')), +); + +expectType( + toThrowErrorMatchingNamedSnapshot.call( + {} as Context, + new Error('received'), + 'snapshot name', + ), +); + +expectType( + toThrowErrorMatchingNamedSnapshot.call( + {} as Context, + new Error('received'), + 'snapshot name', + true, // fromPromise + ), +); + +expectType( + toThrowErrorMatchingNamedSnapshot.call( + {} as Context, + new Error('received'), + undefined, + false, // fromPromise + ), +); + +expectError(toThrowErrorMatchingSnapshot({received: 'value'})); From 14cf63d638fd835cd222d0d2fc3e92b18eb1e874 Mon Sep 17 00:00:00 2001 From: Art Date: Wed, 5 Apr 2023 19:08:01 +0700 Subject: [PATCH 10/32] add e2e concurrent testing --- e2e/__tests__/toMatchNamedSnapshot.test.ts | 48 ++++++++++++++++++++++ e2e/__tests__/toMatchSnapshot.test.ts | 48 ++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/e2e/__tests__/toMatchNamedSnapshot.test.ts b/e2e/__tests__/toMatchNamedSnapshot.test.ts index 906a295bd5be..dbf260ec1765 100644 --- a/e2e/__tests__/toMatchNamedSnapshot.test.ts +++ b/e2e/__tests__/toMatchNamedSnapshot.test.ts @@ -289,3 +289,51 @@ test('handles property matchers with deep properties', () => { expect(exitCode).toBe(1); } }); + +test('support concurrent testing', () => { + const filename = 'match-snapshot-when-test-concurrent.test.js'; + const template = makeTemplate(`describe('group 1', () => { + $1('concurrent 1', async () => { + expect('concurrent 1-1').toMatchNamedSnapshot('test1'); + $2 + }); + + $1('concurrent 2', async () => { + expect('concurrent 1-2').toMatchNamedSnapshot('test2'); + $2 + }); + }); + + describe('group 2', () => { + $1('concurrent 1', async () => { + expect('concurrent 2-1').toMatchNamedSnapshot('test3'); + $2 + }); + + $1('concurrent 2', async () => { + expect('concurrent 2-2').toMatchNamedSnapshot('test4'); + $2 + }); + }); + `); + { + writeFiles(TESTS_DIR, {[filename]: template(['test'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + console.log(stderr); + + expect(exitCode).toBe(0); + } + + { + writeFiles(TESTS_DIR, { + [filename]: template([ + 'test.concurrent', + 'await new Promise(resolve => setTimeout(resolve, 5000));', + ]), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + console.log(stderr); + + expect(exitCode).toBe(0); + } +}); diff --git a/e2e/__tests__/toMatchSnapshot.test.ts b/e2e/__tests__/toMatchSnapshot.test.ts index 7edc3c43cf2b..dd9715c6c38c 100644 --- a/e2e/__tests__/toMatchSnapshot.test.ts +++ b/e2e/__tests__/toMatchSnapshot.test.ts @@ -304,3 +304,51 @@ test('handles property matchers with deep properties', () => { expect(exitCode).toBe(1); } }); + +test('does not support concurrent testing', () => { + const filename = 'match-snapshot-when-test-concurrent.test.js'; + const template = makeTemplate(`describe('group 1', () => { + $1('concurrent 1', async () => { + expect('concurrent 1-1').toMatchSnapshot(); + $2 + }); + + $1('concurrent 2', async () => { + expect('concurrent 1-2').toMatchSnapshot(); + $2 + }); + }); + + describe('group 2', () => { + $1('concurrent 1', async () => { + expect('concurrent 2-1').toMatchSnapshot(); + $2 + }); + + $1('concurrent 2', async () => { + expect('concurrent 2-2').toMatchSnapshot(); + $2 + }); + }); + `); + { + writeFiles(TESTS_DIR, {[filename]: template(['test'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + + expect(stderr).toMatch('4 snapshots written'); + expect(exitCode).toBe(0); + } + + { + writeFiles(TESTS_DIR, { + [filename]: template([ + 'test.concurrent', + 'await new Promise(resolve => setTimeout(resolve, 5000));', + ]), + }); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + + expect(stderr).toMatch('snapshots obsolete'); + expect(exitCode).toBe(1); + } +}); From ab462ead282ec1d1e3a1d4bde134752a26864dca Mon Sep 17 00:00:00 2001 From: Art Date: Fri, 7 Apr 2023 14:54:52 +0700 Subject: [PATCH 11/32] add document --- docs/ExpectAPI.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 20543c996d88..dbb8aa699e96 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -886,6 +886,12 @@ exports[`drinking flavors throws on octopus 1`] = `"yuck, octopus flavor"`; Check out [React Tree Snapshot Testing](/blog/2016/07/27/jest-14) for more information on snapshot testing. +### `.toThrowErrorMatchingNamedSnapshot(snapshotName?)` + +Use `.toThrowErrorMatchingNamedSnapshot` to test that a functino throws an error matching the most recent snapshot when it is called. + +You can provide an optional `snapshotName` string argument that functions as the test name. By providing a snapshot name (as opposed to letting Jest infer the name from context) snapshot names can be guaranteed to be consistent across test runs, whatever the context at the time of evaluation. Jest always appends a number at the end of a snapshot name. + ### `.toThrowErrorMatchingInlineSnapshot(inlineSnapshot)` Use `.toThrowErrorMatchingInlineSnapshot` to test that a function throws an error matching the most recent snapshot when it is called. From c76bf56b63901557cbea909d92abfcad0f7016aa Mon Sep 17 00:00:00 2001 From: Art Date: Sun, 9 Apr 2023 16:41:45 +0700 Subject: [PATCH 12/32] fix lint --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc59dfb95817..d91dfc0901e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ ### Features - `[jest-snapshot]` Add `toMatchNamedSnapshot` to bring snapshot support to concurrent mode + ## 29.5.0 ### Features From c6ca50e9915b1b829bd36f7cb0ed6d9c4b76e70f Mon Sep 17 00:00:00 2001 From: Art Date: Sun, 9 Apr 2023 17:36:52 +0700 Subject: [PATCH 13/32] fixes changelog --- CHANGELOG.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d91dfc0901e9..92876f62c0cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[jest-cli]` Include type definitions to generated config files ([#14078](https://github.com/facebook/jest/pull/14078)) - `[jest-snapshot]` Support arrays as property matchers ([#14025](https://github.com/facebook/jest/pull/14025)) +- `[jest-snapshot]` Add `toMatchNamedSnapshot` to bring snapshot support to concurrent mode ### Fixes @@ -25,12 +26,6 @@ ### Performance -## 29.6.0 - -### Features - -- `[jest-snapshot]` Add `toMatchNamedSnapshot` to bring snapshot support to concurrent mode - ## 29.5.0 ### Features From 198fe2ee212b0baf1cf32d2b49eb6892ee1ecd77 Mon Sep 17 00:00:00 2001 From: Art Date: Mon, 10 Apr 2023 10:57:13 +0700 Subject: [PATCH 14/32] fixes code style --- CHANGELOG.md | 2 +- packages/jest-snapshot/src/index.ts | 2 +- packages/jest-snapshot/src/types.ts | 15 ++++++--------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92876f62c0cb..6c9e1440a587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - `[jest-cli]` Include type definitions to generated config files ([#14078](https://github.com/facebook/jest/pull/14078)) - `[jest-snapshot]` Support arrays as property matchers ([#14025](https://github.com/facebook/jest/pull/14025)) -- `[jest-snapshot]` Add `toMatchNamedSnapshot` to bring snapshot support to concurrent mode +- `[jest-snapshot]` Add `toMatchNamedSnapshot` to bring snapshot support to concurrent mode ([#14045](https://github.com/facebook/jest/pull/14045)) ### Fixes diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index 08932443ad5f..18758df28dc7 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -229,7 +229,7 @@ export const toMatchSnapshot: MatcherFunctionWithContext< export const toMatchNamedSnapshot: MatcherFunctionWithContext< Context, - [propertiesOrSnapshot?: object | string, snapshotName?: string] + [propertiesOrSnapshotName?: object | string, snapshotName?: string] > = function (received, propertiesOrSnapshotName, snapshotName) { const matcherName = 'toMatchNamedSnapshot'; let properties; diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index 69ccae322464..f6218ae888c5 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -57,18 +57,16 @@ export interface SnapshotMatchers, T> { /** * This ensures that a value matches the specific snapshot. * Instead of use current test name in global state, it will use the specific name to find the snapshot. - * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. + */ + toMatchNamedSnapshot(snapshotName?: string): R; + /** + * This ensures that a value matches the specific snapshot with property matchers. + * Instead of use current test name in global state, it will use the specific name to find the snapshot. */ toMatchNamedSnapshot>( propertyMatchers: Partial, snapshot?: string, ): R; - /** - * This ensures that a value matches the specific snapshot. - * Instead of use current test name in global state, it will use the specific name to find the snapshot. - * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. - */ - toMatchNamedSnapshot(snapshotName?: string): R; /** * This ensures that a value matches the most recent snapshot with property matchers. * Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically. @@ -93,12 +91,11 @@ export interface SnapshotMatchers, T> { * Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically. */ toThrowErrorMatchingInlineSnapshot(snapshot?: string): R; - /** * Used to test that a function throws a error matching the specific snapshot. * Instead of use current test name in global state, it will use the specific name to find the snapshot. */ - toThrowErrorMatchingNamedSnapshot(name?: string): R; + toThrowErrorMatchingNamedSnapshot(snapshotName?: string): R; } export type SnapshotFormat = Omit; From 79d3d7d2f95cc35f0314c55ebdc602b4045c66e2 Mon Sep 17 00:00:00 2001 From: Art Date: Mon, 10 Apr 2023 17:17:17 +0700 Subject: [PATCH 15/32] required snapshot name --- .../__typetests__/matchers.test.ts | 21 ++--------- .../__snapshots__/printSnapshot.test.ts.snap | 16 ++++---- .../src/__tests__/matcher.test.ts | 2 +- .../src/__tests__/printSnapshot.test.ts | 35 +++++++++++++----- .../src/__tests__/throwMatcher.test.ts | 2 +- packages/jest-snapshot/src/index.ts | 37 +++++-------------- packages/jest-snapshot/src/types.ts | 6 +-- 7 files changed, 50 insertions(+), 69 deletions(-) diff --git a/packages/jest-snapshot/__typetests__/matchers.test.ts b/packages/jest-snapshot/__typetests__/matchers.test.ts index 88a5a8164c92..c7760a314bdc 100644 --- a/packages/jest-snapshot/__typetests__/matchers.test.ts +++ b/packages/jest-snapshot/__typetests__/matchers.test.ts @@ -82,11 +82,7 @@ expectError(toMatchInlineSnapshot({received: 'value'})); // toMatchNamedSnapshot -expectType( - toMatchNamedSnapshot.call({} as Context, {received: 'value'}), -); - -expectType( +expectError( toMatchNamedSnapshot.call({} as Context, {received: 'value'}), ); @@ -98,7 +94,7 @@ expectType( ), ); -expectType( +expectError( toMatchNamedSnapshot.call( {} as Context, {received: 'value'}, @@ -110,8 +106,8 @@ expectType( toMatchNamedSnapshot.call( {} as Context, {received: 'value'}, - {property: 'match'}, 'snapshot name', + {property: 'match'}, ), ); @@ -187,7 +183,7 @@ expectError(toThrowErrorMatchingInlineSnapshot({received: 'value'})); // toThrowErrorMatchingNamedSnapshot -expectType( +expectError( toThrowErrorMatchingNamedSnapshot.call({} as Context, new Error('received')), ); @@ -208,13 +204,4 @@ expectType( ), ); -expectType( - toThrowErrorMatchingNamedSnapshot.call( - {} as Context, - new Error('received'), - undefined, - false, // fromPromise - ), -); - expectError(toThrowErrorMatchingSnapshot({received: 'value'})); diff --git a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap index 5c4d0c34673e..383adf911fbf 100644 --- a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap +++ b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`change text value 1`] = ` -expect(received).toMatchSnapshot(properties, hint) +expect(received).toMatchNamedSnapshot(properties) -Snapshot name: \`with properties: change text value 1\` +Snapshot name: \`with properties 1\` - Snapshot - 1 + Received + 1 @@ -58,21 +58,19 @@ Expected properties has value: [Function] `; exports[`matcher error toMatchNamedSnapshot Expected properties must be an object (null) with snapshot name 1`] = ` -expect(received).toMatchNamedSnapshot(properties, snapshotName) - -Matcher error: Expected properties must be an object +expect(received).toMatchNamedSnapshot(properties) -Expected properties has value: null +Snapshot state must be initialized -To provide a snapshot name without properties: toMatchNamedSnapshot('snapshotName') +Snapshot state has value: undefined `; exports[`matcher error toMatchNamedSnapshot Expected properties must be an object (null) without snapshot name 1`] = ` expect(received).toMatchNamedSnapshot(properties) -Matcher error: Expected properties must be an object +Snapshot state must be initialized -Expected properties has value: null +Snapshot state has value: undefined `; exports[`matcher error toMatchNamedSnapshot Snapshot state must be initialized 1`] = ` diff --git a/packages/jest-snapshot/src/__tests__/matcher.test.ts b/packages/jest-snapshot/src/__tests__/matcher.test.ts index 96dac4c0a48c..ec57928e60ca 100644 --- a/packages/jest-snapshot/src/__tests__/matcher.test.ts +++ b/packages/jest-snapshot/src/__tests__/matcher.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {type Context, toMatchSnapshot} from '../'; +import {Context, toMatchSnapshot} from '../'; test('returns matcher name, expected and actual values', () => { const mockedContext = { diff --git a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts index 0206d0b2fbae..037c32f66775 100644 --- a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts +++ b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts @@ -289,9 +289,11 @@ describe('matcher error', () => { promise: '', } as Context; const properties = () => {}; + const snapshotName = + 'toMatchNamedSnapshot Expected properties must be an object (non-null)'; expect(() => { - toMatchNamedSnapshot.call(context, received, properties); + toMatchNamedSnapshot.call(context, received, snapshotName, properties); }).toThrowErrorMatchingSnapshot(); }); @@ -305,7 +307,7 @@ describe('matcher error', () => { expect(() => { // @ts-expect-error: Testing runtime error - toMatchNamedSnapshot.call(context, received, properties, snapshotName); + toMatchNamedSnapshot.call(context, received, snapshotName, properties); }).toThrowErrorMatchingSnapshot(); }); @@ -315,10 +317,12 @@ describe('matcher error', () => { promise: '', } as Context; const properties = null; + const snapshotName = + 'toMatchNamedSnapshot Expected properties must be an object (null) without snapshot name'; expect(() => { // @ts-expect-error: Testing runtime error - toMatchNamedSnapshot.call(context, received, properties); + toMatchNamedSnapshot.call(context, received, snapshotName, properties); }).toThrowErrorMatchingSnapshot(); }); @@ -328,9 +332,11 @@ describe('matcher error', () => { promise: '', } as Context; const properties: Array = []; + const snapshotName = + 'toMatchNamedSnapshot Expected properties must be an object (null) without snapshot name'; expect(() => { - toMatchNamedSnapshot.call(context, received, properties); + toMatchNamedSnapshot.call(context, received, snapshotName, properties); }).toThrowErrorMatchingSnapshot(); }); @@ -342,16 +348,25 @@ describe('matcher error', () => { snapshotState: {}, } as Context; const properties = {}; + const snapshotName = + 'toMatchNamedSnapshot received value must be an object'; test('(non-null)', () => { expect(() => { - toMatchNamedSnapshot.call(context, 'string', properties); + toMatchNamedSnapshot.call( + context, + 'string', + snapshotName, + properties, + ); }).toThrowErrorMatchingSnapshot(); }); test('(null)', () => { + const snapshotName = 'toMatchNamedSnapshot (null)'; + expect(() => { - toMatchNamedSnapshot.call(context, null, properties); + toMatchNamedSnapshot.call(context, null, snapshotName, properties); }).toThrowErrorMatchingSnapshot(); }); }); @@ -831,8 +846,8 @@ describe('pass false', () => { const {message, pass} = toMatchNamedSnapshot.call( context, new RangeError('Invalid array length'), - {name: 'Error'}, snapshotName, + {name: 'Error'}, ) as SyncExpectationResult; expect(pass).toBe(false); expect(message()).toMatchNamedSnapshot(snapshotName); @@ -849,8 +864,8 @@ describe('pass false', () => { const {message, pass} = toMatchNamedSnapshot.call( context, received, - properties, snapshotName, + properties, ) as SyncExpectationResult; expect(pass).toBe(false); expect(message()).toMatchNamedSnapshot(snapshotName); @@ -890,11 +905,11 @@ describe('pass false', () => { }; const snapshotName = 'change text value'; - const {message, pass} = toMatchSnapshot.call( + const {message, pass} = toMatchNamedSnapshot.call( context, received, - properties, snapshotName, + properties, ) as SyncExpectationResult; expect(pass).toBe(false); expect(message()).toMatchNamedSnapshot(snapshotName); diff --git a/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts b/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts index b7b35a60073c..3846092742da 100644 --- a/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts +++ b/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts @@ -47,7 +47,7 @@ describe('throw matcher can take func', () => { () => { throw new Error('coconut'); }, - undefined, + '', false, ); diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index 18758df28dc7..e88f1fa15945 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -229,40 +229,26 @@ export const toMatchSnapshot: MatcherFunctionWithContext< export const toMatchNamedSnapshot: MatcherFunctionWithContext< Context, - [propertiesOrSnapshotName?: object | string, snapshotName?: string] -> = function (received, propertiesOrSnapshotName, snapshotName) { + [snapshotName: string, properties?: object] +> = function (received, snapshotName, properties?) { const matcherName = 'toMatchNamedSnapshot'; - let properties; - const length = arguments.length; - if (length === 2 && typeof propertiesOrSnapshotName === 'string') { - snapshotName = propertiesOrSnapshotName; - } else if (length >= 2) { + if (properties) { if ( - Array.isArray(propertiesOrSnapshotName) || - typeof propertiesOrSnapshotName !== 'object' || - propertiesOrSnapshotName === null + Array.isArray(properties) || + typeof properties !== 'object' || + properties === null ) { const options: MatcherHintOptions = { isNot: this.isNot, promise: this.promise, }; - let printedWithType = printWithType( + const printedWithType = printWithType( 'Expected properties', - propertiesOrSnapshotName, + properties, printExpected, ); - if (length === 3) { - options.secondArgument = 'snapshotName'; - options.secondArgumentColor = BOLD_WEIGHT; - - if (propertiesOrSnapshotName == null) { - printedWithType += - "\n\nTo provide a snapshot name without properties: toMatchNamedSnapshot('snapshotName')"; - } - } - throw new Error( matcherErrorMessage( matcherHint(matcherName, undefined, PROPERTIES_ARG, options), @@ -271,11 +257,6 @@ export const toMatchNamedSnapshot: MatcherFunctionWithContext< ), ); } - - // Future breaking change: Snapshot hint must be a string - // if (arguments.length === 3 && typeof hint !== 'string') {} - - properties = propertiesOrSnapshotName; } return _toMatchSnapshot({ @@ -548,7 +529,7 @@ export const toThrowErrorMatchingInlineSnapshot: MatcherFunctionWithContext< export const toThrowErrorMatchingNamedSnapshot: MatcherFunctionWithContext< Context, - [snapshotName?: string, fromPromise?: boolean] + [snapshotName: string, fromPromise?: boolean] > = function (received, snapshotName, fromPromise) { const matcherName = 'toThrowErrorMatchingNamedSnapshot'; diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index f6218ae888c5..69713b76d46f 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -58,14 +58,14 @@ export interface SnapshotMatchers, T> { * This ensures that a value matches the specific snapshot. * Instead of use current test name in global state, it will use the specific name to find the snapshot. */ - toMatchNamedSnapshot(snapshotName?: string): R; + toMatchNamedSnapshot(snapshotName: string): R; /** * This ensures that a value matches the specific snapshot with property matchers. * Instead of use current test name in global state, it will use the specific name to find the snapshot. */ toMatchNamedSnapshot>( + snapshot: string, propertyMatchers: Partial, - snapshot?: string, ): R; /** * This ensures that a value matches the most recent snapshot with property matchers. @@ -95,7 +95,7 @@ export interface SnapshotMatchers, T> { * Used to test that a function throws a error matching the specific snapshot. * Instead of use current test name in global state, it will use the specific name to find the snapshot. */ - toThrowErrorMatchingNamedSnapshot(snapshotName?: string): R; + toThrowErrorMatchingNamedSnapshot(snapshotName: string): R; } export type SnapshotFormat = Omit; From 6b1a6ed70f7a42d7740c01f3852593bcb54d7333 Mon Sep 17 00:00:00 2001 From: Art Date: Mon, 10 Apr 2023 17:45:14 +0700 Subject: [PATCH 16/32] fixes properties --- .../__snapshots__/printSnapshot.test.ts.snap | 12 ++---------- .../src/__tests__/printSnapshot.test.ts | 17 +---------------- packages/jest-snapshot/src/index.ts | 2 +- 3 files changed, 4 insertions(+), 27 deletions(-) diff --git a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap index 383adf911fbf..f6d7649f705e 100644 --- a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap +++ b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap @@ -60,17 +60,9 @@ Expected properties has value: [Function] exports[`matcher error toMatchNamedSnapshot Expected properties must be an object (null) with snapshot name 1`] = ` expect(received).toMatchNamedSnapshot(properties) -Snapshot state must be initialized - -Snapshot state has value: undefined -`; - -exports[`matcher error toMatchNamedSnapshot Expected properties must be an object (null) without snapshot name 1`] = ` -expect(received).toMatchNamedSnapshot(properties) - -Snapshot state must be initialized +Matcher error: Expected properties must be an object -Snapshot state has value: undefined +Expected properties has value: null `; exports[`matcher error toMatchNamedSnapshot Snapshot state must be initialized 1`] = ` diff --git a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts index 037c32f66775..64582f78111f 100644 --- a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts +++ b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts @@ -311,21 +311,6 @@ describe('matcher error', () => { }).toThrowErrorMatchingSnapshot(); }); - test('Expected properties must be an object (null) without snapshot name', () => { - const context = { - isNot: false, - promise: '', - } as Context; - const properties = null; - const snapshotName = - 'toMatchNamedSnapshot Expected properties must be an object (null) without snapshot name'; - - expect(() => { - // @ts-expect-error: Testing runtime error - toMatchNamedSnapshot.call(context, received, snapshotName, properties); - }).toThrowErrorMatchingSnapshot(); - }); - test('Expected properties must be an object (array)', () => { const context = { isNot: false, @@ -333,7 +318,7 @@ describe('matcher error', () => { } as Context; const properties: Array = []; const snapshotName = - 'toMatchNamedSnapshot Expected properties must be an object (null) without snapshot name'; + 'toMatchNamedSnapshot Expected properties must be an object (array)'; expect(() => { toMatchNamedSnapshot.call(context, received, snapshotName, properties); diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index e88f1fa15945..d8d4093a826e 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -233,7 +233,7 @@ export const toMatchNamedSnapshot: MatcherFunctionWithContext< > = function (received, snapshotName, properties?) { const matcherName = 'toMatchNamedSnapshot'; - if (properties) { + if (properties !== undefined) { if ( Array.isArray(properties) || typeof properties !== 'object' || From 735e50305941e29773285f6e95153fbb0e0ecf2f Mon Sep 17 00:00:00 2001 From: Art Date: Mon, 10 Apr 2023 17:57:49 +0700 Subject: [PATCH 17/32] edit e2e testing --- e2e/__tests__/toMatchNamedSnapshot.test.ts | 21 ++++++++++--------- .../toMatchNamedSnapshotWithRetries.test.ts | 4 ++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/e2e/__tests__/toMatchNamedSnapshot.test.ts b/e2e/__tests__/toMatchNamedSnapshot.test.ts index dbf260ec1765..9bd967a757cd 100644 --- a/e2e/__tests__/toMatchNamedSnapshot.test.ts +++ b/e2e/__tests__/toMatchNamedSnapshot.test.ts @@ -188,7 +188,7 @@ test('accepts custom snapshot name', () => { test('handles property matchers', () => { const filename = 'handle-property-matchers.test.js'; const template = makeTemplate(`test('handles property matchers', () => { - expect({createdAt: $1}).toMatchNamedSnapshot({createdAt: expect.any(Date)}, 'property-matchers'); + expect({createdAt: $1}).toMatchNamedSnapshot('property-matchers',{createdAt: expect.any(Date)}); }); `); @@ -215,11 +215,11 @@ test('handles property matchers', () => { }); test('handles invalid property matchers', () => { - const filename = 'handle-property-matchers.test.js'; + const filename = 'handle-invalid-property-matchers.test.js'; { writeFiles(TESTS_DIR, { [filename]: `test('invalid property matchers', () => { - expect({foo: 'bar'}).toMatchNamedSnapshot(null, 'null-property-matcher'); + expect({foo: 'bar'}).toMatchNamedSnapshot('null-property-matcher', null); }); `, }); @@ -227,19 +227,20 @@ test('handles invalid property matchers', () => { expect(stderr).toMatch('Expected properties must be an object'); expect(exitCode).toBe(1); } +}); + +test('handles undefined property matchers', () => { + const filename = 'handle-undefined-property-matchers.test.js'; { writeFiles(TESTS_DIR, { [filename]: `test('invalid property matchers', () => { - expect({foo: 'bar'}).toMatchNamedSnapshot(undefined, 'undefined-property-matcher'); + expect({foo: 'bar'}).toMatchNamedSnapshot('undefined-property-matcher', undefined); }); `, }); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch('Expected properties must be an object'); - expect(stderr).toMatch( - "To provide a snapshot name without properties: toMatchNamedSnapshot('snapshotName')", - ); - expect(exitCode).toBe(1); + expect(stderr).toMatch('1 snapshot written'); + expect(exitCode).toBe(0); } }); @@ -247,7 +248,7 @@ test('handles property matchers with deep properties', () => { const filename = 'handle-property-matchers-with-name.test.js'; const template = makeTemplate(`test('handles property matchers with deep properties', () => { - expect({ user: { createdAt: $1, name: $2 }}).toMatchNamedSnapshot({ user: { createdAt: expect.any(Date), name: $2 }}, 'deep-property-matchers'); + expect({ user: { createdAt: $1, name: $2 }}).toMatchNamedSnapshot('deep-property-matchers', { user: { createdAt: expect.any(Date), name: $2 }}); }); `); diff --git a/e2e/__tests__/toMatchNamedSnapshotWithRetries.test.ts b/e2e/__tests__/toMatchNamedSnapshotWithRetries.test.ts index b5557f614825..957d6a83afb1 100644 --- a/e2e/__tests__/toMatchNamedSnapshotWithRetries.test.ts +++ b/e2e/__tests__/toMatchNamedSnapshotWithRetries.test.ts @@ -61,14 +61,14 @@ test('works with a single snapshot', () => { test('works when multiple tests have snapshots but only one of them failed multiple times', () => { const filename = 'basic-support.test.js'; const template = makeTemplate(` - test('passing snapshots', () => expect('foo').toMatchNamedSnapshot()); + test('passing snapshots', () => expect('foo').toMatchNamedSnapshot('passing snapshots')); describe('with retries', () => { let index = 0; afterEach(() => { index += 1; }); jest.retryTimes($2); - test('snapshots', () => expect($1).toMatchNamedSnapshot()); + test('snapshots', () => expect($1).toMatchNamedSnapshot('with retries')); }); `); From ed4e6057765ca8da3e7a81fb2a03f06a7f828134 Mon Sep 17 00:00:00 2001 From: Art Date: Mon, 10 Apr 2023 18:13:19 +0700 Subject: [PATCH 18/32] add e2e type test --- .../jest-types/__typetests__/expect.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/jest-types/__typetests__/expect.test.ts b/packages/jest-types/__typetests__/expect.test.ts index dbed505d98df..7300bb59261c 100644 --- a/packages/jest-types/__typetests__/expect.test.ts +++ b/packages/jest-types/__typetests__/expect.test.ts @@ -415,6 +415,38 @@ expectError( }), ); +expectType(expect('abc').toMatchNamedSnapshot('snapshot name')); + +expectType( + expect({ + date: new Date(), + name: 'John Doe', + }).toMatchNamedSnapshot('snapshot name', { + date: expect.any(Date), + name: expect.any(String), + }), +); + +expectType( + expect({ + date: new Date(), + name: 'John Doe', + }).toMatchNamedSnapshot('snapshot name', { + date: expect.any(Date), + name: expect.any(String), + }), +); + +expectError( + expect({ + date: new Date(), + name: 'John Doe', + }).toMatchNamedSnapshot('snapshot name', { + date: expect.any(Date), + time: expect.any(Date), + }), +); + expectType(expect(jest.fn()).toThrowErrorMatchingSnapshot()); expectType(expect(jest.fn()).toThrowErrorMatchingSnapshot('hint')); expectError(expect(jest.fn()).toThrowErrorMatchingSnapshot(true)); From e3dc8fc7656728b0e878dd57034f62ae5786ed8b Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 11 Apr 2023 00:38:28 +0700 Subject: [PATCH 19/32] add more e2e type test --- packages/jest-types/__typetests__/expect.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/jest-types/__typetests__/expect.test.ts b/packages/jest-types/__typetests__/expect.test.ts index 7300bb59261c..8dac94b2a5f2 100644 --- a/packages/jest-types/__typetests__/expect.test.ts +++ b/packages/jest-types/__typetests__/expect.test.ts @@ -416,6 +416,7 @@ expectError( ); expectType(expect('abc').toMatchNamedSnapshot('snapshot name')); +expectError(expect('abc').toMatchNamedSnapshot()); expectType( expect({ @@ -457,6 +458,11 @@ expectType( ); expectError(expect(jest.fn()).toThrowErrorMatchingInlineSnapshot(true)); +expectType( + expect(jest.fn()).toThrowErrorMatchingNamedSnapshot('snapshot-name'), +); +expectError(expect(jest.fn()).toThrowErrorMatchingNamedSnapshot()); + // extend type MatcherUtils = typeof jestMatcherUtils & { From 7e7527aff7ec92a3302fd407dd0c889515371b82 Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 11 Apr 2023 00:39:46 +0700 Subject: [PATCH 20/32] remove unused overload --- packages/jest-snapshot/src/types.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index 69713b76d46f..9acc0196ae36 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -54,18 +54,13 @@ export interface SnapshotMatchers, T> { propertyMatchers: Partial, hint?: string, ): R; - /** - * This ensures that a value matches the specific snapshot. - * Instead of use current test name in global state, it will use the specific name to find the snapshot. - */ - toMatchNamedSnapshot(snapshotName: string): R; /** * This ensures that a value matches the specific snapshot with property matchers. * Instead of use current test name in global state, it will use the specific name to find the snapshot. */ toMatchNamedSnapshot>( - snapshot: string, - propertyMatchers: Partial, + snapshotName: string, + propertyMatchers?: Partial, ): R; /** * This ensures that a value matches the most recent snapshot with property matchers. From cbbec0a6b9791da3e6bc6678828182cca8165d19 Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 11 Apr 2023 00:40:16 +0700 Subject: [PATCH 21/32] add validation on snapshot name --- e2e/__tests__/toMatchNamedSnapshot.test.ts | 17 +++++++ .../toThrowErrorMatchingNamedSnapshot.test.ts | 16 ++++++ packages/jest-snapshot/src/index.ts | 50 ++++++++++++++++--- 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/e2e/__tests__/toMatchNamedSnapshot.test.ts b/e2e/__tests__/toMatchNamedSnapshot.test.ts index 9bd967a757cd..049817a35c87 100644 --- a/e2e/__tests__/toMatchNamedSnapshot.test.ts +++ b/e2e/__tests__/toMatchNamedSnapshot.test.ts @@ -170,6 +170,23 @@ test('mark snapshots as obsolete in skipped tests if snapshot name does not matc } }); +test('throws the error if snapshot name is not string', () => { + const filename = 'no-obsolete-if-skipped.test.js'; + const template = makeTemplate(` + test('will be error', () => { + expect({a: 6}).toMatchNamedSnapshot(true); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template(['test.skip'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + console.log(stderr); + expect(stderr).toMatch('Expected snapshotName must be a string'); + expect(exitCode).toBe(1); + } +}); + test('accepts custom snapshot name', () => { const filename = 'accept-custom-snapshot-name.test.js'; const template = makeTemplate(`test('accepts custom snapshot name', () => { diff --git a/e2e/__tests__/toThrowErrorMatchingNamedSnapshot.test.ts b/e2e/__tests__/toThrowErrorMatchingNamedSnapshot.test.ts index d3ea510df58b..2fae119c79b8 100644 --- a/e2e/__tests__/toThrowErrorMatchingNamedSnapshot.test.ts +++ b/e2e/__tests__/toThrowErrorMatchingNamedSnapshot.test.ts @@ -52,6 +52,22 @@ test("throws the error if tested function didn't throw error", () => { } }); +test('throws the error if snapshot name is not string', () => { + const filename = 'throws-if-tested-function-did-not-throw.test.js'; + const template = + makeTemplate(`test('throws the error if snapshot name is not string', () => { + expect(() => { throw new Error('apple'); }).toThrowErrorMatchingNamedSnapshot(true); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch('Expected snapshotName must be a string'); + expect(exitCode).toBe(1); + } +}); + test('accepts custom snapshot name', () => { const filename = 'accept-custom-snapshot-name.test.js'; const template = makeTemplate(`test('accepts custom snapshot name', () => { diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index d8d4093a826e..5d15ec18026f 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -230,15 +230,31 @@ export const toMatchSnapshot: MatcherFunctionWithContext< export const toMatchNamedSnapshot: MatcherFunctionWithContext< Context, [snapshotName: string, properties?: object] -> = function (received, snapshotName, properties?) { +> = function (received: unknown, snapshotName: unknown, properties?: unknown) { const matcherName = 'toMatchNamedSnapshot'; + if (typeof snapshotName !== 'string') { + const options: MatcherHintOptions = { + isNot: this.isNot, + promise: this.promise, + }; + const printedWithType = printWithType( + 'Expected snapshotName', + snapshotName, + printExpected, + ); + + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, PROPERTIES_ARG, options), + `Expected ${EXPECTED_COLOR('snapshotName')} must be a string`, + printedWithType, + ), + ); + } + if (properties !== undefined) { - if ( - Array.isArray(properties) || - typeof properties !== 'object' || - properties === null - ) { + if (typeof properties !== 'object' || properties === null) { const options: MatcherHintOptions = { isNot: this.isNot, promise: this.promise, @@ -530,9 +546,29 @@ export const toThrowErrorMatchingInlineSnapshot: MatcherFunctionWithContext< export const toThrowErrorMatchingNamedSnapshot: MatcherFunctionWithContext< Context, [snapshotName: string, fromPromise?: boolean] -> = function (received, snapshotName, fromPromise) { +> = function (received: unknown, snapshotName: unknown, fromPromise: unknown) { const matcherName = 'toThrowErrorMatchingNamedSnapshot'; + if (typeof snapshotName !== 'string') { + const options: MatcherHintOptions = { + isNot: this.isNot, + promise: this.promise, + }; + const printedWithType = printWithType( + 'Expected snapshotName', + snapshotName, + printExpected, + ); + + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, PROPERTIES_ARG, options), + `Expected ${EXPECTED_COLOR('snapshotName')} must be a string`, + printedWithType, + ), + ); + } + return _toThrowErrorMatchingSnapshot( { context: this, From 77ae43629d09c45e0b7441bb1115cd2fb53c776b Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 11 Apr 2023 00:55:43 +0700 Subject: [PATCH 22/32] support array in properties matcher --- .../__snapshots__/printSnapshot.test.ts.snap | 9 --------- .../src/__tests__/printSnapshot.test.ts | 14 -------------- 2 files changed, 23 deletions(-) diff --git a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap index f6d7649f705e..3c0529649b18 100644 --- a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap +++ b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap @@ -90,15 +90,6 @@ exports[`matcher error toMatchNamedSnapshot received value must be an object (nu Received has value: null `; -exports[`matcher error toMatchSnapshot Expected properties must be an object (array) 1`] = ` -expect(received).toMatchSnapshot(properties) - -Matcher error: Expected properties must be an object - -Expected properties has type: array -Expected properties has value: [] -`; - exports[`matcher error toMatchSnapshot Expected properties must be an object (non-null) 1`] = ` expect(received).toMatchSnapshot(properties) diff --git a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts index 64582f78111f..6689fb6ce21e 100644 --- a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts +++ b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts @@ -311,20 +311,6 @@ describe('matcher error', () => { }).toThrowErrorMatchingSnapshot(); }); - test('Expected properties must be an object (array)', () => { - const context = { - isNot: false, - promise: '', - } as Context; - const properties: Array = []; - const snapshotName = - 'toMatchNamedSnapshot Expected properties must be an object (array)'; - - expect(() => { - toMatchNamedSnapshot.call(context, received, snapshotName, properties); - }).toThrowErrorMatchingSnapshot(); - }); - describe('received value must be an object', () => { const context = { currentTestName: '', From b7b81a149d24c5ff97f0f6f3f14f51d0f2c8ecbd Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 11 Apr 2023 00:56:49 +0700 Subject: [PATCH 23/32] remove unused function --- e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts b/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts index e02e394919a6..aa62422e6238 100644 --- a/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts +++ b/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts @@ -93,7 +93,6 @@ test('should support rejecting promises', () => { { writeFiles(TESTS_DIR, {[filename]: template()}); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); - console.log(stderr); const snapshot = fs.readFileSync( `${TESTS_DIR}/__snapshots__/${filename}.snap`, From 6327019d5f9cdb17810acf587793b9d151ed86ea Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 11 Apr 2023 00:59:32 +0700 Subject: [PATCH 24/32] Apply suggestions from code review Co-authored-by: Tom Mrazauskas --- docs/ExpectAPI.md | 10 +++++----- docs/SnapshotTesting.md | 6 +++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index dbb8aa699e96..7a7722efe2df 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -744,13 +744,13 @@ You can provide an optional `propertyMatchers` object argument, which has asymme 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. -### `.toMatchNamedSnapshot(propertyMatchers?, snapshotName?)` +### `.toMatchNamedSnapshot(snapshotName, propertyMatchers?)` 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 functions as the test name. By providing a snapshot name (as opposed to letting Jest infer the name from context) snapshot names can be guaranteed to be consistent across test runs, whatever the context at the time of evaluation. Jest always appends a number at the end of a snapshot name. +By setting the `snapshotName` explicitly (as opposed to letting Jest infer the name from context) it can be guaranteed to be consistent across test runs, whatever the context at the time of evaluation. Jest always appends a number at the end of a snapshot name. Jest sorts snapshots by name in the corresponding `.snap` file. @@ -886,11 +886,11 @@ exports[`drinking flavors throws on octopus 1`] = `"yuck, octopus flavor"`; Check out [React Tree Snapshot Testing](/blog/2016/07/27/jest-14) for more information on snapshot testing. -### `.toThrowErrorMatchingNamedSnapshot(snapshotName?)` +### `.toThrowErrorMatchingNamedSnapshot(snapshotName)` -Use `.toThrowErrorMatchingNamedSnapshot` to test that a functino throws an error matching the most recent snapshot when it is called. +Use `.toThrowErrorMatchingNamedSnapshot` 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 functions as the test name. By providing a snapshot name (as opposed to letting Jest infer the name from context) snapshot names can be guaranteed to be consistent across test runs, whatever the context at the time of evaluation. Jest always appends a number at the end of a snapshot name. +By setting the `snapshotName` explicitly (as opposed to letting Jest infer the name from context) it can be guaranteed to be consistent across test runs, whatever the context at the time of evaluation. Jest always appends a number at the end of a snapshot name. ### `.toThrowErrorMatchingInlineSnapshot(inlineSnapshot)` diff --git a/docs/SnapshotTesting.md b/docs/SnapshotTesting.md index 23ae76570326..69594f37c050 100644 --- a/docs/SnapshotTesting.md +++ b/docs/SnapshotTesting.md @@ -264,7 +264,11 @@ Now, every time the snapshot test case runs, `Date.now()` will return `148236336 Always strive to use descriptive test and/or snapshot names for snapshots. The best names describe the expected snapshot content. This makes it easier for reviewers to verify the snapshots during review, and for anyone to know whether or not an outdated snapshot is the correct behavior before updating. -The default matcher, `toMatchSnapshot`, infers the name based on the test function context when the snapshot is evaluated. This can cause snapshots in tests that are run concurrently to have different names on each test run. If you want to guarantee consistent names, you can use `toMatchNamedSnapshot` as an alternative matcher. +:::tip + +The `toMatchSnapshot` matcher infers the name based on the test function context when the snapshot is evaluated. This can cause snapshots in tests that are run concurrently to have different names on each test run. If you want to guarantee consistent names, you can use the `toMatchNamedSnapshot` matcher. + +::: For example, compare: From 58acde3f48ff78282c849edc43564de58fa62da7 Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 11 Apr 2023 12:32:47 +0700 Subject: [PATCH 25/32] remove fromPromise type --- packages/jest-snapshot/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index 5d15ec18026f..ac4d1144bd33 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -546,7 +546,7 @@ export const toThrowErrorMatchingInlineSnapshot: MatcherFunctionWithContext< export const toThrowErrorMatchingNamedSnapshot: MatcherFunctionWithContext< Context, [snapshotName: string, fromPromise?: boolean] -> = function (received: unknown, snapshotName: unknown, fromPromise: unknown) { +> = function (received: unknown, snapshotName: unknown, fromPromise) { const matcherName = 'toThrowErrorMatchingNamedSnapshot'; if (typeof snapshotName !== 'string') { From 23d8053be0cd97682e005b56988f9686fe46954d Mon Sep 17 00:00:00 2001 From: Art Date: Sun, 16 Apr 2023 13:47:04 +0700 Subject: [PATCH 26/32] update tip on snapshot testing document --- docs/SnapshotTesting.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/SnapshotTesting.md b/docs/SnapshotTesting.md index 69594f37c050..71ce23573925 100644 --- a/docs/SnapshotTesting.md +++ b/docs/SnapshotTesting.md @@ -52,6 +52,12 @@ More information on how snapshot testing works and why we built it can be found ::: +:::tip + +The `toMatchSnapshot` matcher infers the name based on the test function context when the snapshot is evaluated. This can cause snapshots in tests that are run concurrently to have different names on each test run. If you want to guarantee consistent names, you can use the `toMatchNamedSnapshot` matcher. + +::: + ### Updating Snapshots It's straightforward to spot when a snapshot test fails after a bug has been introduced. When that happens, go ahead and fix the issue and make sure your snapshot tests are passing again. Now, let's talk about the case when a snapshot test is failing due to an intentional implementation change. @@ -264,10 +270,6 @@ Now, every time the snapshot test case runs, `Date.now()` will return `148236336 Always strive to use descriptive test and/or snapshot names for snapshots. The best names describe the expected snapshot content. This makes it easier for reviewers to verify the snapshots during review, and for anyone to know whether or not an outdated snapshot is the correct behavior before updating. -:::tip - -The `toMatchSnapshot` matcher infers the name based on the test function context when the snapshot is evaluated. This can cause snapshots in tests that are run concurrently to have different names on each test run. If you want to guarantee consistent names, you can use the `toMatchNamedSnapshot` matcher. - ::: For example, compare: From 78ddf16135417754d674ee2d60c1cbc32cb182f3 Mon Sep 17 00:00:00 2001 From: Art Date: Sun, 16 Apr 2023 14:01:32 +0700 Subject: [PATCH 27/32] Fixes alphabetic order --- packages/jest-snapshot/src/__tests__/matcher.test.ts | 2 +- packages/jest-snapshot/src/index.ts | 6 +++--- packages/jest-snapshot/src/types.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/jest-snapshot/src/__tests__/matcher.test.ts b/packages/jest-snapshot/src/__tests__/matcher.test.ts index ec57928e60ca..96dac4c0a48c 100644 --- a/packages/jest-snapshot/src/__tests__/matcher.test.ts +++ b/packages/jest-snapshot/src/__tests__/matcher.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {Context, toMatchSnapshot} from '../'; +import {type Context, toMatchSnapshot} from '../'; test('returns matcher name, expected and actual values', () => { const mockedContext = { diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index ac4d1144bd33..1942da639340 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -73,7 +73,7 @@ const printSnapshotName = ( }; const getSnapshotName = (config: SnapshotNameConfig): string => { - const {snapshotName, currentTestName, hint} = config; + const {currentTestName, hint, snapshotName} = config; if (snapshotName) { return snapshotName; @@ -355,8 +355,8 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => { inlineSnapshot, isInline, matcherName, - snapshotName, properties, + snapshotName, } = config; let {received} = config; @@ -591,8 +591,8 @@ const _toThrowErrorMatchingSnapshot = ( inlineSnapshot, isInline, matcherName, - snapshotName, received, + snapshotName, } = config; context.dontThrow && context.dontThrow(); diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index 9acc0196ae36..aafaa6abf161 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -27,15 +27,15 @@ export type MatchSnapshotConfig = { inlineSnapshot?: string; isInline: boolean; matcherName: string; - snapshotName?: string; properties?: object; received: any; + snapshotName?: string; }; export type SnapshotNameConfig = { + currentTestName?: string; hint?: string; snapshotName?: string; - currentTestName?: string; }; export type SnapshotData = Record; From 435bdc8ad255aeea01874caec0e19b198fdb116b Mon Sep 17 00:00:00 2001 From: Art Date: Sun, 16 Apr 2023 14:01:44 +0700 Subject: [PATCH 28/32] Fixes docs --- docs/ExpectAPI.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 7a7722efe2df..6acd9b76a4a9 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -750,7 +750,7 @@ This ensures that a value matches the most recent snapshot. Check out [the Snaps 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. -By setting the `snapshotName` explicitly (as opposed to letting Jest infer the name from context) it can be guaranteed to be consistent across test runs, whatever the context at the time of evaluation. Jest always appends a number at the end of a snapshot name. +By setting the `snapshotName` explicitly (as opposed to letting Jest infer the name from context) it can be guaranteed to be consistent across test runs, whatever the context at the time of evaluation. Jest always appends a number at the end of a snapshot name. Jest sorts snapshots by name in the corresponding `.snap` file. From 9fccf7d642a24995d199aa39b780e4b92f9bdad1 Mon Sep 17 00:00:00 2001 From: Art Date: Sun, 16 Apr 2023 17:43:36 +0700 Subject: [PATCH 29/32] Update docs/SnapshotTesting.md Co-authored-by: Tom Mrazauskas --- docs/SnapshotTesting.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/SnapshotTesting.md b/docs/SnapshotTesting.md index 71ce23573925..5795245e2556 100644 --- a/docs/SnapshotTesting.md +++ b/docs/SnapshotTesting.md @@ -270,8 +270,6 @@ Now, every time the snapshot test case runs, `Date.now()` will return `148236336 Always strive to use descriptive test and/or snapshot names for snapshots. The best names describe the expected snapshot content. This makes it easier for reviewers to verify the snapshots during review, and for anyone to know whether or not an outdated snapshot is the correct behavior before updating. -::: - For example, compare: ```js From 30370706e6ce18f06c026369adf9ac03132b82cb Mon Sep 17 00:00:00 2001 From: Art Date: Wed, 14 Jun 2023 15:46:58 +0700 Subject: [PATCH 30/32] Throw error when duplicate snapshot name --- e2e/__tests__/toMatchNamedSnapshot.test.ts | 22 ++++++++++++++++++++++ packages/jest-snapshot/src/index.ts | 16 ++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/e2e/__tests__/toMatchNamedSnapshot.test.ts b/e2e/__tests__/toMatchNamedSnapshot.test.ts index 049817a35c87..2b385821ba73 100644 --- a/e2e/__tests__/toMatchNamedSnapshot.test.ts +++ b/e2e/__tests__/toMatchNamedSnapshot.test.ts @@ -308,6 +308,28 @@ test('handles property matchers with deep properties', () => { } }); +test('error duplicate snapshot name', () => { + const filename = 'duplicate-match-named-snapshot.test.js'; + const template = makeTemplate( + `test('duplicate named snapshots', () => { + expect($1).toMatchNamedSnapshot('basic-support'); + expect($1).toMatchNamedSnapshot('basic-support'); + }); + `, + ); + { + writeFiles(TESTS_DIR, {[filename]: template(['test'])}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + console.log(stderr); + + expect(stderr).toMatch( + 'The specific snapshot name was duplicate with the other snapshot.', + ); + expect(stderr).toMatch('Snapshot name: basic-support'); + expect(exitCode).toBe(1); + } +}); + test('support concurrent testing', () => { const filename = 'match-snapshot-when-test-concurrent.test.js'; const template = makeTemplate(`describe('group 1', () => { diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index 1942da639340..80b865b61ae6 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -36,7 +36,12 @@ import type { MatchSnapshotConfig, SnapshotNameConfig, } from './types'; -import {deepMerge, escapeBacktickString, serialize} from './utils'; +import { + deepMerge, + escapeBacktickString, + serialize, + testNameToKey, +} from './utils'; export {addSerializer, getSerializers} from './plugins'; export { @@ -439,7 +444,14 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => { received, testName: fullTestName, }); - const {actual, count, expected, pass} = result; + const {actual, count, expected, key, pass} = result; + + if (snapshotName && key == testNameToKey(fullTestName, 1)) { + throw new Error( + 'The specific snapshot name was duplicate with the other snapshot.\n\n' + + `Snapshot name: ${snapshotName}`, + ); + } if (pass) { return {message: () => '', pass: true}; From 5e5800958fd6bad1edb33110fe8b2050a7b67a76 Mon Sep 17 00:00:00 2001 From: Art Date: Thu, 15 Jun 2023 14:01:48 +0700 Subject: [PATCH 31/32] fix test --- e2e/__tests__/toMatchNamedSnapshot.test.ts | 2 +- packages/jest-snapshot/src/__tests__/printSnapshot.test.ts | 3 +++ packages/jest-snapshot/src/index.ts | 5 +++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/e2e/__tests__/toMatchNamedSnapshot.test.ts b/e2e/__tests__/toMatchNamedSnapshot.test.ts index 2b385821ba73..148d5f8f8c91 100644 --- a/e2e/__tests__/toMatchNamedSnapshot.test.ts +++ b/e2e/__tests__/toMatchNamedSnapshot.test.ts @@ -94,7 +94,7 @@ test('first snapshot fails, second passes', () => { const filename = 'first-snapshot-fails-second-passes.test.js'; const template = makeTemplate(`test('named snapshots', () => { expect($1).toMatchNamedSnapshot('test-snapshot'); - expect($2).toMatchNamedSnapshot('test-snapshot'); + expect($2).toMatchNamedSnapshot('test-snapshot-2'); });`); { diff --git a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts index 6689fb6ce21e..2e9bbf49dcb2 100644 --- a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts +++ b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts @@ -745,6 +745,7 @@ describe('pass false', () => { actual: format(received), count: 1, expected: undefined, + key: 'specific-not-written-multi-line 1', pass: false, }; }, @@ -773,6 +774,7 @@ describe('pass false', () => { actual: format(received), count: 2, expected: undefined, + key: 'specific-not-written-single-line 1', pass: false, }; }, @@ -860,6 +862,7 @@ describe('pass false', () => { text: 'snapshot', type, }), + key: 'change text value 1', pass: false, }; }, diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index 80b865b61ae6..bf5dbe3ac95a 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -446,10 +446,11 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => { }); const {actual, count, expected, key, pass} = result; - if (snapshotName && key == testNameToKey(fullTestName, 1)) { + if (snapshotName && key != testNameToKey(fullTestName, 1)) { throw new Error( 'The specific snapshot name was duplicate with the other snapshot.\n\n' + - `Snapshot name: ${snapshotName}`, + `Snapshot name: ${fullTestName}\n` + + `Snapshot Key: ${key}`, ); } From acb42b03ccccf9f8a6fdc4dc959ac86ef5d78c00 Mon Sep 17 00:00:00 2001 From: Art Date: Fri, 16 Jun 2023 09:47:25 +0700 Subject: [PATCH 32/32] fix the test --- .../src/__tests__/throwMatcher.test.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts b/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts index 3846092742da..4c639d1b19e5 100644 --- a/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts +++ b/packages/jest-snapshot/src/__tests__/throwMatcher.test.ts @@ -92,16 +92,26 @@ describe('throw matcher from promise', () => { }); describe('toThrowErrorMatchingNamedSnapshot', () => { + const mockedNamedMatch = jest.fn(() => ({ + actual: 'coconut', + expected: 'coconut', + key: 'snapshot name 1', + })); + + const mockedNamedContext = { + snapshotState: {match: mockedNamedMatch}, + } as unknown as Context; + it('can take error', () => { toThrowErrorMatchingNamedSnapshot.call( - mockedContext, + mockedNamedContext, new Error('coco'), 'snapshot name', true, ); - expect(mockedMatch).toHaveBeenCalledTimes(1); - expect(mockedMatch).toHaveBeenCalledWith( + expect(mockedNamedMatch).toHaveBeenCalledTimes(1); + expect(mockedNamedMatch).toHaveBeenCalledWith( expect.objectContaining({received: 'coco', testName: 'snapshot name'}), ); }); @@ -110,14 +120,14 @@ describe('throw matcher from promise', () => { class CustomError extends Error {} toThrowErrorMatchingNamedSnapshot.call( - mockedContext, + mockedNamedContext, new CustomError('nut'), 'snapshot name', true, ); - expect(mockedMatch).toHaveBeenCalledTimes(1); - expect(mockedMatch).toHaveBeenCalledWith( + expect(mockedNamedMatch).toHaveBeenCalledTimes(1); + expect(mockedNamedMatch).toHaveBeenCalledWith( expect.objectContaining({received: 'nut', testName: 'snapshot name'}), ); });