From 4ecea8e70d5b11b905e26e83f3ee64fba2848969 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Tue, 26 Nov 2019 10:36:36 +0800 Subject: [PATCH 01/14] Add support for custom inline snapshot matchers --- packages/expect/src/externalMatcherMark.ts | 9 +++++++++ packages/expect/src/index.ts | 12 ++++++++---- packages/jest-snapshot/src/State.ts | 5 ++++- packages/jest-snapshot/src/inline_snapshots.ts | 15 ++++++++++----- packages/jest-snapshot/src/utils.ts | 13 +++++++++++++ 5 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 packages/expect/src/externalMatcherMark.ts diff --git a/packages/expect/src/externalMatcherMark.ts b/packages/expect/src/externalMatcherMark.ts new file mode 100644 index 000000000000..e832febe6830 --- /dev/null +++ b/packages/expect/src/externalMatcherMark.ts @@ -0,0 +1,9 @@ +/** + * This file is intentionally created for marking external matchers with + * an explicit function call in the stack trace, benefiting the inline snapshot + * system to correctly strip out user defined code when trying to locate the + * top frame. This is required for custom inline snapshot matchers to work. + */ +export default function noop(any: T): T { + return any; +} diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index 3bb418c79298..33918206287f 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -47,6 +47,7 @@ import { setState, } from './jestMatchersObject'; import extractExpectedAssertionsErrors from './extractExpectedAssertionsErrors'; +import externalMatcherMark from './externalMatcherMark'; class JestAssertionError extends Error { matcherResult?: SyncExpectationResult; @@ -297,7 +298,7 @@ const makeThrowingMatcher = ( } }; - const handlError = (error: Error) => { + const handleError = (error: Error) => { if ( matcher[INTERNAL_MATCHER_FLAG] === true && !(error instanceof JestAssertionError) && @@ -314,7 +315,10 @@ const makeThrowingMatcher = ( let potentialResult: ExpectationResult; try { - potentialResult = matcher.call(matcherContext, actual, ...args); + potentialResult = + matcher[INTERNAL_MATCHER_FLAG] === true + ? matcher.call(matcherContext, actual, ...args) + : externalMatcherMark(matcher.call(matcherContext, actual, ...args)); if (isPromise(potentialResult)) { const asyncResult = potentialResult as AsyncExpectationResult; @@ -325,14 +329,14 @@ const makeThrowingMatcher = ( return asyncResult .then(aResult => processResult(aResult, asyncError)) - .catch(error => handlError(error)); + .catch(error => handleError(error)); } else { const syncResult = potentialResult as SyncExpectationResult; return processResult(syncResult); } } catch (error) { - return handlError(error); + return handleError(error); } }; diff --git a/packages/jest-snapshot/src/State.ts b/packages/jest-snapshot/src/State.ts index af3e659ab54f..c9c592b60d37 100644 --- a/packages/jest-snapshot/src/State.ts +++ b/packages/jest-snapshot/src/State.ts @@ -14,6 +14,7 @@ import { getSnapshotData, keyToTestName, removeExtraLineBreaks, + removeLinesBeforeExternalMatcherMark, saveSnapshotFile, serialize, testNameToKey, @@ -104,7 +105,9 @@ export default class SnapshotState { this._dirty = true; if (options.isInline) { const error = options.error || new Error(); - const lines = getStackTraceLines(error.stack || ''); + const lines = getStackTraceLines( + removeLinesBeforeExternalMatcherMark(error.stack || ''), + ); const frame = getTopFrame(lines); if (!frame) { throw new Error( diff --git a/packages/jest-snapshot/src/inline_snapshots.ts b/packages/jest-snapshot/src/inline_snapshots.ts index aee39a282d5a..21d67dfb7d0c 100644 --- a/packages/jest-snapshot/src/inline_snapshots.ts +++ b/packages/jest-snapshot/src/inline_snapshots.ts @@ -92,7 +92,7 @@ const saveSnapshotsForFile = ( const formattedNewSourceFile = prettier.format(newSourceFile, { ...config, filepath: sourceFilePath, - parser: createFormattingParser(inferredParser, babelTraverse), + parser: createFormattingParser(snapshots, inferredParser, babelTraverse), }); if (formattedNewSourceFile !== sourceFile) { @@ -228,6 +228,7 @@ const createInsertionParser = ( // This parser formats snapshots to the correct indentation. const createFormattingParser = ( + snapshots: Array, inferredParser: string, babelTraverse: Function, ) => ( @@ -238,18 +239,22 @@ const createFormattingParser = ( // Workaround for https://github.com/prettier/prettier/issues/3150 options.parser = inferredParser; + const groupedSnapshots = groupSnapshotsByFrame(snapshots); + const ast = getAst(parsers, inferredParser, text); babelTraverse(ast, { CallExpression({node: {arguments: args, callee}}: {node: CallExpression}) { if ( callee.type !== 'MemberExpression' || - callee.property.type !== 'Identifier' || - callee.property.name !== 'toMatchInlineSnapshot' || - !callee.loc || - callee.computed + callee.property.type !== 'Identifier' ) { return; } + const {line, column} = callee.property.loc.start; + const snapshotsForFrame = groupedSnapshots[`${line}:${column}`]; + if (!snapshotsForFrame) { + return; + } let snapshotIndex: number | undefined; let snapshot: string | undefined; diff --git a/packages/jest-snapshot/src/utils.ts b/packages/jest-snapshot/src/utils.ts index 8d9f6a8374f4..4aeaf75abfaa 100644 --- a/packages/jest-snapshot/src/utils.ts +++ b/packages/jest-snapshot/src/utils.ts @@ -136,6 +136,19 @@ export const removeExtraLineBreaks = (string: string): string => ? string.slice(1, -1) : string; +export const removeLinesBeforeExternalMatcherMark = (stack: string): string => { + const lines = stack.split('\n'); + + for (let i = 0; i < lines.length; i += 1) { + if (lines[i].match(/\/externalMatcherMark\.js:\d+:\d+\)$/)) { + lines.splice(1, i); + break; + } + } + + return lines.join('\n'); +}; + const escapeRegex = true; const printFunctionName = false; From cb39b4acf0577aff7546191a7462802869cf1490 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Fri, 6 Dec 2019 17:44:38 +0800 Subject: [PATCH 02/14] Use token to identify inline snapshot matchers --- .../jest-snapshot/src/inline_snapshots.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/jest-snapshot/src/inline_snapshots.ts b/packages/jest-snapshot/src/inline_snapshots.ts index 21d67dfb7d0c..c393346483b0 100644 --- a/packages/jest-snapshot/src/inline_snapshots.ts +++ b/packages/jest-snapshot/src/inline_snapshots.ts @@ -19,6 +19,8 @@ import {Frame} from 'jest-message-util'; import {Config} from '@jest/types'; import {escapeBacktickString} from './utils'; +const SNAPSHOT_TOKEN = `$__SNAPSHOT_TOKEN__$`; + export type InlineSnapshot = { snapshot: string; frame: Frame; @@ -92,7 +94,7 @@ const saveSnapshotsForFile = ( const formattedNewSourceFile = prettier.format(newSourceFile, { ...config, filepath: sourceFilePath, - parser: createFormattingParser(snapshots, inferredParser, babelTraverse), + parser: createFormattingParser(inferredParser, babelTraverse), }); if (formattedNewSourceFile !== sourceFile) { @@ -198,6 +200,7 @@ const createInsertionParser = ( 'Jest: Multiple inline snapshots for the same call are not supported.', ); } + callee.property.name = SNAPSHOT_TOKEN + callee.property.name; const snapshotIndex = args.findIndex( ({type}) => type === 'TemplateLiteral', ); @@ -228,7 +231,6 @@ const createInsertionParser = ( // This parser formats snapshots to the correct indentation. const createFormattingParser = ( - snapshots: Array, inferredParser: string, babelTraverse: Function, ) => ( @@ -239,22 +241,20 @@ const createFormattingParser = ( // Workaround for https://github.com/prettier/prettier/issues/3150 options.parser = inferredParser; - const groupedSnapshots = groupSnapshotsByFrame(snapshots); - const ast = getAst(parsers, inferredParser, text); babelTraverse(ast, { CallExpression({node: {arguments: args, callee}}: {node: CallExpression}) { if ( callee.type !== 'MemberExpression' || - callee.property.type !== 'Identifier' + callee.property.type !== 'Identifier' || + !callee.property.name.startsWith(SNAPSHOT_TOKEN) || + !callee.loc || + callee.computed ) { return; } - const {line, column} = callee.property.loc.start; - const snapshotsForFrame = groupedSnapshots[`${line}:${column}`]; - if (!snapshotsForFrame) { - return; - } + + callee.property.name = callee.property.name.slice(SNAPSHOT_TOKEN.length); let snapshotIndex: number | undefined; let snapshot: string | undefined; From aa505706c697fe2c348246530a90a877a29226d4 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sat, 7 Dec 2019 13:28:15 +0800 Subject: [PATCH 03/14] Use array to record matcher names --- .../toMatchInlineSnapshot.test.ts.snap | 17 ++++++++++ e2e/__tests__/toMatchInlineSnapshot.test.ts | 22 +++++++++++++ packages/expect/src/index.ts | 5 +-- packages/jest-snapshot/src/State.ts | 4 +-- .../jest-snapshot/src/inline_snapshots.ts | 31 ++++++++++++++----- packages/jest-snapshot/src/utils.ts | 4 +-- 6 files changed, 69 insertions(+), 14 deletions(-) diff --git a/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap b/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap index 67f659ceb42c..181615dd5ba6 100644 --- a/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap +++ b/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap @@ -160,6 +160,23 @@ test('inline snapshots', async () => { `; +exports[`supports custom matchers: custom matchers 1`] = ` +const {toMatchInlineSnapshot} = require('jest-snapshot'); +expect.extend({ + toMatchCustomInlineSnapshot(received, ...args) { + return toMatchInlineSnapshot.call(this, received, ...args); + }, +}); +test('inline snapshots', () => { + expect({apple: 'original value'}).toMatchCustomInlineSnapshot(\` + Object { + "apple": "original value", + } + \`); +}); + +`; + exports[`writes snapshots with non-literals in expect(...) 1`] = ` it('works with inline snapshots', () => { expect({a: 1}).toMatchInlineSnapshot(\` diff --git a/e2e/__tests__/toMatchInlineSnapshot.test.ts b/e2e/__tests__/toMatchInlineSnapshot.test.ts index bea20f9baf04..bfde1e4c0f2e 100644 --- a/e2e/__tests__/toMatchInlineSnapshot.test.ts +++ b/e2e/__tests__/toMatchInlineSnapshot.test.ts @@ -266,3 +266,25 @@ test('handles mocking native modules prettier relies on', () => { expect(stderr).toMatch('1 snapshot written from 1 test suite.'); expect(exitCode).toBe(0); }); + +test('supports custom matchers', () => { + const filename = 'custom-matchers.test.js'; + const test = ` + const { toMatchInlineSnapshot } = require('jest-snapshot'); + expect.extend({ + toMatchCustomInlineSnapshot(received, ...args) { + return toMatchInlineSnapshot.call(this, received, ...args); + } + }); + test('inline snapshots', () => { + expect({apple: "original value"}).toMatchCustomInlineSnapshot(); + }); + `; + + writeFiles(TESTS_DIR, {[filename]: test}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(exitCode).toBe(0); + expect(wrap(fileAfter)).toMatchSnapshot('custom matchers'); +}); diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index 33918206287f..489d9f453eef 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -47,7 +47,6 @@ import { setState, } from './jestMatchersObject'; import extractExpectedAssertionsErrors from './extractExpectedAssertionsErrors'; -import externalMatcherMark from './externalMatcherMark'; class JestAssertionError extends Error { matcherResult?: SyncExpectationResult; @@ -318,7 +317,9 @@ const makeThrowingMatcher = ( potentialResult = matcher[INTERNAL_MATCHER_FLAG] === true ? matcher.call(matcherContext, actual, ...args) - : externalMatcherMark(matcher.call(matcherContext, actual, ...args)); + : (function __EXTERNAL_MATCHER_TRAP__() { + return matcher.call(matcherContext, actual, ...args); + })(); if (isPromise(potentialResult)) { const asyncResult = potentialResult as AsyncExpectationResult; diff --git a/packages/jest-snapshot/src/State.ts b/packages/jest-snapshot/src/State.ts index c9c592b60d37..da0ee884a8db 100644 --- a/packages/jest-snapshot/src/State.ts +++ b/packages/jest-snapshot/src/State.ts @@ -14,7 +14,7 @@ import { getSnapshotData, keyToTestName, removeExtraLineBreaks, - removeLinesBeforeExternalMatcherMark, + removeLinesBeforeExternalMatcherTrap, saveSnapshotFile, serialize, testNameToKey, @@ -106,7 +106,7 @@ export default class SnapshotState { if (options.isInline) { const error = options.error || new Error(); const lines = getStackTraceLines( - removeLinesBeforeExternalMatcherMark(error.stack || ''), + removeLinesBeforeExternalMatcherTrap(error.stack || ''), ); const frame = getTopFrame(lines); if (!frame) { diff --git a/packages/jest-snapshot/src/inline_snapshots.ts b/packages/jest-snapshot/src/inline_snapshots.ts index c393346483b0..aef9e08130a4 100644 --- a/packages/jest-snapshot/src/inline_snapshots.ts +++ b/packages/jest-snapshot/src/inline_snapshots.ts @@ -19,8 +19,6 @@ import {Frame} from 'jest-message-util'; import {Config} from '@jest/types'; import {escapeBacktickString} from './utils'; -const SNAPSHOT_TOKEN = `$__SNAPSHOT_TOKEN__$`; - export type InlineSnapshot = { snapshot: string; frame: Frame; @@ -80,6 +78,10 @@ const saveSnapshotsForFile = ( ? prettier.getFileInfo.sync(sourceFilePath).inferredParser : (config && config.parser) || simpleDetectParser(sourceFilePath); + // Record the matcher names seen in insertion parser and pass them down one + // by one to formatting parser. + const snapshotMatcherNames: Array = []; + // Insert snapshots using the custom parser API. After insertion, the code is // formatted, except snapshot indentation. Snapshots cannot be formatted until // after the initial format because we don't know where the call expression @@ -87,14 +89,23 @@ const saveSnapshotsForFile = ( const newSourceFile = prettier.format(sourceFile, { ...config, filepath: sourceFilePath, - parser: createInsertionParser(snapshots, inferredParser, babelTraverse), + parser: createInsertionParser( + snapshots, + snapshotMatcherNames, + inferredParser, + babelTraverse, + ), }); // Format the snapshots using the custom parser API. const formattedNewSourceFile = prettier.format(newSourceFile, { ...config, filepath: sourceFilePath, - parser: createFormattingParser(inferredParser, babelTraverse), + parser: createFormattingParser( + snapshotMatcherNames, + inferredParser, + babelTraverse, + ), }); if (formattedNewSourceFile !== sourceFile) { @@ -168,6 +179,7 @@ const getAst = ( // This parser inserts snapshots into the AST. const createInsertionParser = ( snapshots: Array, + snapshotMatcherNames: Array, inferredParser: string, babelTraverse: Function, ) => ( @@ -200,7 +212,9 @@ const createInsertionParser = ( 'Jest: Multiple inline snapshots for the same call are not supported.', ); } - callee.property.name = SNAPSHOT_TOKEN + callee.property.name; + + snapshotMatcherNames.push(callee.property.name); + const snapshotIndex = args.findIndex( ({type}) => type === 'TemplateLiteral', ); @@ -231,6 +245,7 @@ const createInsertionParser = ( // This parser formats snapshots to the correct indentation. const createFormattingParser = ( + snapshotMatcherNames: Array, inferredParser: string, babelTraverse: Function, ) => ( @@ -247,15 +262,13 @@ const createFormattingParser = ( if ( callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier' || - !callee.property.name.startsWith(SNAPSHOT_TOKEN) || + callee.property.name !== snapshotMatcherNames[0] || !callee.loc || callee.computed ) { return; } - callee.property.name = callee.property.name.slice(SNAPSHOT_TOKEN.length); - let snapshotIndex: number | undefined; let snapshot: string | undefined; for (let i = 0; i < args.length; i++) { @@ -269,6 +282,8 @@ const createFormattingParser = ( return; } + snapshotMatcherNames.shift(); + const useSpaces = !options.useTabs; snapshot = indent( snapshot, diff --git a/packages/jest-snapshot/src/utils.ts b/packages/jest-snapshot/src/utils.ts index 4aeaf75abfaa..13bc4e0130c1 100644 --- a/packages/jest-snapshot/src/utils.ts +++ b/packages/jest-snapshot/src/utils.ts @@ -136,11 +136,11 @@ export const removeExtraLineBreaks = (string: string): string => ? string.slice(1, -1) : string; -export const removeLinesBeforeExternalMatcherMark = (stack: string): string => { +export const removeLinesBeforeExternalMatcherTrap = (stack: string): string => { const lines = stack.split('\n'); for (let i = 0; i < lines.length; i += 1) { - if (lines[i].match(/\/externalMatcherMark\.js:\d+:\d+\)$/)) { + if (lines[i].includes('__EXTERNAL_MATCHER_TRAP__')) { lines.splice(1, i); break; } From 40f34db818bc2722f4dd4b5351ac464bb45ae14c Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sat, 7 Dec 2019 13:29:25 +0800 Subject: [PATCH 04/14] Delete matcher mark file --- packages/expect/src/externalMatcherMark.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 packages/expect/src/externalMatcherMark.ts diff --git a/packages/expect/src/externalMatcherMark.ts b/packages/expect/src/externalMatcherMark.ts deleted file mode 100644 index e832febe6830..000000000000 --- a/packages/expect/src/externalMatcherMark.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This file is intentionally created for marking external matchers with - * an explicit function call in the stack trace, benefiting the inline snapshot - * system to correctly strip out user defined code when trying to locate the - * top frame. This is required for custom inline snapshot matchers to work. - */ -export default function noop(any: T): T { - return any; -} From c581940a6075dcaf13df6572badce8c65ae3db3e Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sat, 7 Dec 2019 14:08:09 +0800 Subject: [PATCH 05/14] Add comments --- packages/expect/src/index.ts | 5 ++++- packages/jest-snapshot/src/utils.ts | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index 489d9f453eef..a99fa819d0e3 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -317,7 +317,10 @@ const makeThrowingMatcher = ( potentialResult = matcher[INTERNAL_MATCHER_FLAG] === true ? matcher.call(matcherContext, actual, ...args) - : (function __EXTERNAL_MATCHER_TRAP__() { + : // It's a trap specifically for inline snapshot to capture this name + // in the stack trace, so that it can correctly get the custom matcher + // function call. + (function __EXTERNAL_MATCHER_TRAP__() { return matcher.call(matcherContext, actual, ...args); })(); diff --git a/packages/jest-snapshot/src/utils.ts b/packages/jest-snapshot/src/utils.ts index 13bc4e0130c1..405a2fb42f56 100644 --- a/packages/jest-snapshot/src/utils.ts +++ b/packages/jest-snapshot/src/utils.ts @@ -140,6 +140,8 @@ export const removeLinesBeforeExternalMatcherTrap = (stack: string): string => { const lines = stack.split('\n'); for (let i = 0; i < lines.length; i += 1) { + // It's a function name specified in `packages/expect/src/index.ts` + // for external custom matchers. if (lines[i].includes('__EXTERNAL_MATCHER_TRAP__')) { lines.splice(1, i); break; From 7d1b9f51962bec8dc04c611475cc71877cb9a293 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sat, 7 Dec 2019 16:14:19 +0800 Subject: [PATCH 06/14] Make `removeLinesBeforeExternalMatcherTrap` clearer for what it does Co-Authored-By: Simen Bekkhus --- packages/jest-snapshot/src/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jest-snapshot/src/utils.ts b/packages/jest-snapshot/src/utils.ts index 405a2fb42f56..ca7d5448dec8 100644 --- a/packages/jest-snapshot/src/utils.ts +++ b/packages/jest-snapshot/src/utils.ts @@ -143,12 +143,12 @@ export const removeLinesBeforeExternalMatcherTrap = (stack: string): string => { // It's a function name specified in `packages/expect/src/index.ts` // for external custom matchers. if (lines[i].includes('__EXTERNAL_MATCHER_TRAP__')) { - lines.splice(1, i); + return lines.slice(i).join('\n'); break; } } - return lines.join('\n'); + return lines; }; const escapeRegex = true; From 52f9234e3c6eda640b465f0965c3eabef1868369 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sat, 7 Dec 2019 16:15:41 +0800 Subject: [PATCH 07/14] Add unit tests for `removeLinesBeforeExternalMatcherTrap` --- .../jest-snapshot/src/__tests__/utils.test.ts | 38 +++++++++++++++++++ packages/jest-snapshot/src/utils.ts | 5 +-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/jest-snapshot/src/__tests__/utils.test.ts b/packages/jest-snapshot/src/__tests__/utils.test.ts index 4fcdf587da1a..df0845440f49 100644 --- a/packages/jest-snapshot/src/__tests__/utils.test.ts +++ b/packages/jest-snapshot/src/__tests__/utils.test.ts @@ -24,6 +24,7 @@ import { getSnapshotData, keyToTestName, removeExtraLineBreaks, + removeLinesBeforeExternalMatcherTrap, saveSnapshotFile, serialize, testNameToKey, @@ -266,6 +267,43 @@ describe('ExtraLineBreaks', () => { }); }); +describe('removeLinesBeforeExternalMatcherTrap', () => { + test('contains external matcher trap', () => { + const stack = `Error: + at SnapshotState._addSnapshot (/jest/packages/jest-snapshot/build/State.js:150:9) + at SnapshotState.match (/jest/packages/jest-snapshot/build/State.js:303:14) + at _toMatchSnapshot (/jest/packages/jest-snapshot/build/index.js:399:32) + at _toThrowErrorMatchingSnapshot (/jest/packages/jest-snapshot/build/index.js:585:10) + at Object.toThrowErrorMatchingInlineSnapshot (/jest/packages/jest-snapshot/build/index.js:504:10) + at Object. (/jest/packages/expect/build/index.js:138:20) + at __EXTERNAL_MATCHER_TRAP__ (/jest/packages/expect/build/index.js:378:30) + at throwingMatcher (/jest/packages/expect/build/index.js:379:15) + at /jest/packages/expect/build/index.js:285:72 + at Object. (/jest/e2e/to-throw-error-matching-inline-snapshot/__tests__/should-support-rejecting-promises.test.js:3:7)`; + + const expected = ` at throwingMatcher (/jest/packages/expect/build/index.js:379:15) + at /jest/packages/expect/build/index.js:285:72 + at Object. (/jest/e2e/to-throw-error-matching-inline-snapshot/__tests__/should-support-rejecting-promises.test.js:3:7)`; + + expect(removeLinesBeforeExternalMatcherTrap(stack)).toBe(expected); + }); + + test("doesn't contain external matcher trap", () => { + const stack = `Error: + at SnapshotState._addSnapshot (/jest/packages/jest-snapshot/build/State.js:150:9) + at SnapshotState.match (/jest/packages/jest-snapshot/build/State.js:303:14) + at _toMatchSnapshot (/jest/packages/jest-snapshot/build/index.js:399:32) + at _toThrowErrorMatchingSnapshot (/jest/packages/jest-snapshot/build/index.js:585:10) + at Object.toThrowErrorMatchingInlineSnapshot (/jest/packages/jest-snapshot/build/index.js:504:10) + at Object. (/jest/packages/expect/build/index.js:138:20) + at throwingMatcher (/jest/packages/expect/build/index.js:379:15) + at /jest/packages/expect/build/index.js:285:72 + at Object. (/jest/e2e/to-throw-error-matching-inline-snapshot/__tests__/should-support-rejecting-promises.test.js:3:7)`; + + expect(removeLinesBeforeExternalMatcherTrap(stack)).toBe(stack); + }); +}); + describe('DeepMerge with property matchers', () => { const matcher = expect.any(String); diff --git a/packages/jest-snapshot/src/utils.ts b/packages/jest-snapshot/src/utils.ts index ca7d5448dec8..c5c9a31c3acd 100644 --- a/packages/jest-snapshot/src/utils.ts +++ b/packages/jest-snapshot/src/utils.ts @@ -143,12 +143,11 @@ export const removeLinesBeforeExternalMatcherTrap = (stack: string): string => { // It's a function name specified in `packages/expect/src/index.ts` // for external custom matchers. if (lines[i].includes('__EXTERNAL_MATCHER_TRAP__')) { - return lines.slice(i).join('\n'); - break; + return lines.slice(i + 1).join('\n'); } } - return lines; + return lines.join('\n'); }; const escapeRegex = true; From 83eb242449544f15bee743046d87ab731dc3ae63 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sat, 7 Dec 2019 18:00:31 +0800 Subject: [PATCH 08/14] Directly return stack without joining Co-Authored-By: Simen Bekkhus --- packages/jest-snapshot/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-snapshot/src/utils.ts b/packages/jest-snapshot/src/utils.ts index c5c9a31c3acd..602f6812a718 100644 --- a/packages/jest-snapshot/src/utils.ts +++ b/packages/jest-snapshot/src/utils.ts @@ -147,7 +147,7 @@ export const removeLinesBeforeExternalMatcherTrap = (stack: string): string => { } } - return lines.join('\n'); + return stack; }; const escapeRegex = true; From 32f4c5ec0e43571ccfa3eb92c6658e68c14dba28 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sat, 7 Dec 2019 18:11:25 +0800 Subject: [PATCH 09/14] Fix typo --- packages/jest-snapshot/src/inline_snapshots.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-snapshot/src/inline_snapshots.ts b/packages/jest-snapshot/src/inline_snapshots.ts index aef9e08130a4..e54d62ba08c6 100644 --- a/packages/jest-snapshot/src/inline_snapshots.ts +++ b/packages/jest-snapshot/src/inline_snapshots.ts @@ -133,7 +133,7 @@ const groupSnapshotsByFile = groupSnapshotsBy(({frame: {file}}) => file); const indent = (snapshot: string, numIndents: number, indentation: string) => { const lines = snapshot.split('\n'); - // Prevent re-identation of inline snapshots. + // Prevent re-indentation of inline snapshots. if ( lines.length >= 2 && lines[1].startsWith(indentation.repeat(numIndents + 1)) From d530b1c7e29af3e3f90cfb21f7e4a44cedb9fb1b Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sat, 7 Dec 2019 18:27:25 +0800 Subject: [PATCH 10/14] Add e2e tests for multiple inline snapshot matchers --- .../toMatchInlineSnapshot.test.ts.snap | 35 +++++++++++++++++++ e2e/__tests__/toMatchInlineSnapshot.test.ts | 28 +++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap b/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap index 181615dd5ba6..987948aa7deb 100644 --- a/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap +++ b/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap @@ -120,6 +120,41 @@ test('handles property matchers', () => { `; +exports[`multiple custom matchers and native matchers: multiple matchers 1`] = ` +const {toMatchInlineSnapshot} = require('jest-snapshot'); +expect.extend({ + toMatchCustomInlineSnapshot(received, ...args) { + return toMatchInlineSnapshot.call(this, received, ...args); + }, + toMatchCustomInlineSnapshot2(received, ...args) { + return toMatchInlineSnapshot.call(this, received, ...args); + }, +}); +test('inline snapshots', () => { + expect({apple: 'value 1'}).toMatchCustomInlineSnapshot(\` + Object { + "apple": "value 1", + } + \`); + expect({apple: 'value 2'}).toMatchInlineSnapshot(\` + Object { + "apple": "value 2", + } + \`); + expect({apple: 'value 3'}).toMatchCustomInlineSnapshot2(\` + Object { + "apple": "value 3", + } + \`); + expect({apple: 'value 4'}).toMatchInlineSnapshot(\` + Object { + "apple": "value 4", + } + \`); +}); + +`; + exports[`removes obsolete external snapshots: external snapshot cleaned 1`] = ` test('removes obsolete external snapshots', () => { expect('1').toMatchInlineSnapshot(\`"1"\`); diff --git a/e2e/__tests__/toMatchInlineSnapshot.test.ts b/e2e/__tests__/toMatchInlineSnapshot.test.ts index bfde1e4c0f2e..9a81893981ae 100644 --- a/e2e/__tests__/toMatchInlineSnapshot.test.ts +++ b/e2e/__tests__/toMatchInlineSnapshot.test.ts @@ -288,3 +288,31 @@ test('supports custom matchers', () => { expect(exitCode).toBe(0); expect(wrap(fileAfter)).toMatchSnapshot('custom matchers'); }); + +test('multiple custom matchers and native matchers', () => { + const filename = 'multiple-matchers.test.js'; + const test = ` + const { toMatchInlineSnapshot } = require('jest-snapshot'); + expect.extend({ + toMatchCustomInlineSnapshot(received, ...args) { + return toMatchInlineSnapshot.call(this, received, ...args); + }, + toMatchCustomInlineSnapshot2(received, ...args) { + return toMatchInlineSnapshot.call(this, received, ...args); + }, + }); + test('inline snapshots', () => { + expect({apple: "value 1"}).toMatchCustomInlineSnapshot(); + expect({apple: "value 2"}).toMatchInlineSnapshot(); + expect({apple: "value 3"}).toMatchCustomInlineSnapshot2(); + expect({apple: "value 4"}).toMatchInlineSnapshot(); + }); + `; + + writeFiles(TESTS_DIR, {[filename]: test}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('4 snapshots written from 1 test suite.'); + expect(exitCode).toBe(0); + expect(wrap(fileAfter)).toMatchSnapshot('multiple matchers'); +}); From e6431c4f736e14aca7a33aee7e2672a2eb6ab883 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sat, 7 Dec 2019 19:00:07 +0800 Subject: [PATCH 11/14] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc41106eef2..bf7cde1ed9aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[expect, jest-snapshot]` Support custom inline snapshot matchers ([#9278](https://github.com/facebook/jest/pull/9278)) - `[babel-plugin-jest-hoist]` Show codeframe on static hoisting issues ([#8865](https://github.com/facebook/jest/pull/8865)) - `[babel-plugin-jest-hoist]` Add `BigInt` to `WHITELISTED_IDENTIFIERS` ([#8382](https://github.com/facebook/jest/pull/8382)) - `[babel-preset-jest]` Add `@babel/plugin-syntax-bigint` ([#8382](https://github.com/facebook/jest/pull/8382)) From 4046b237d56da488738024c7d69b58800ae6670a Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sun, 8 Dec 2019 02:12:25 +0800 Subject: [PATCH 12/14] Update CHANGELOG to sort it in alphabetical order --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf7cde1ed9aa..225de647270c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,12 @@ ### Features -- `[expect, jest-snapshot]` Support custom inline snapshot matchers ([#9278](https://github.com/facebook/jest/pull/9278)) - `[babel-plugin-jest-hoist]` Show codeframe on static hoisting issues ([#8865](https://github.com/facebook/jest/pull/8865)) - `[babel-plugin-jest-hoist]` Add `BigInt` to `WHITELISTED_IDENTIFIERS` ([#8382](https://github.com/facebook/jest/pull/8382)) - `[babel-preset-jest]` Add `@babel/plugin-syntax-bigint` ([#8382](https://github.com/facebook/jest/pull/8382)) - `[expect]` Add `BigInt` support to `toBeGreaterThan`, `toBeGreaterThanOrEqual`, `toBeLessThan` and `toBeLessThanOrEqual` ([#8382](https://github.com/facebook/jest/pull/8382)) - `[expect, jest-matcher-utils]` Display change counts in annotation lines ([#9035](https://github.com/facebook/jest/pull/9035)) +- `[expect, jest-snapshot]` Support custom inline snapshot matchers ([#9278](https://github.com/facebook/jest/pull/9278)) - `[jest-config]` Throw the full error message and stack when a Jest preset is missing a dependency ([#8924](https://github.com/facebook/jest/pull/8924)) - `[jest-config]` [**BREAKING**] Set default display name color based on runner ([#8689](https://github.com/facebook/jest/pull/8689)) - `[jest-config]` Merge preset globals with project globals ([#9027](https://github.com/facebook/jest/pull/9027)) From 7e7e51a7841f088777e55211098e180740b7e7e4 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sun, 8 Dec 2019 02:12:43 +0800 Subject: [PATCH 13/14] Update doc --- docs/ExpectAPI.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 9c58b49a97fc..af1721ebf32a 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -233,6 +233,28 @@ exports[`stores only 10 characters: toMatchTrimmedSnapshot 1`] = `"extra long"`; */ ``` +It's also possible to create custom matchers for inline snapshots, the snapshots will be correctly added to the custom matchers. However, inline snapshot will always try to append to the first argument or the second when the first argument is the property matcher, so it's not possible to accept custom arguments in the custom matchers. + +```js +const {toMatchInlineSnapshot} = require('jest-snapshot'); + +expect.extend({ + toMatchTrimmedInlineSnapshot(received) { + return toMatchInlineSnapshot.call(this, received.substring(0, 10)); + }, +}); + +it('stores only 10 characters', () => { + expect('extra long string oh my gerd').toMatchTrimmedInlineSnapshot(); + /* + The snapshot will be added inline like + expect('extra long string oh my gerd').toMatchTrimmedInlineSnapshot( + `"extra long"` + ); + */ +}); +``` + ### `expect.anything()` `expect.anything()` matches anything but `null` or `undefined`. You can use it inside `toEqual` or `toBeCalledWith` instead of a literal value. For example, if you want to check that a mock function is called with a non-null argument: From 495efd39617667403e1ef61ec2c2e557aab20e9b Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sun, 8 Dec 2019 02:14:20 +0800 Subject: [PATCH 14/14] Add e2e tests for property matchers --- .../toMatchInlineSnapshot.test.ts.snap | 48 +++++++++++++++++++ e2e/__tests__/toMatchInlineSnapshot.test.ts | 44 +++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap b/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap index 987948aa7deb..b2d7e3c2a5a1 100644 --- a/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap +++ b/e2e/__tests__/__snapshots__/toMatchInlineSnapshot.test.ts.snap @@ -195,6 +195,54 @@ test('inline snapshots', async () => { `; +exports[`supports custom matchers with property matcher: custom matchers with property matcher 1`] = ` +const {toMatchInlineSnapshot} = require('jest-snapshot'); +expect.extend({ + toMatchCustomInlineSnapshot(received, ...args) { + return toMatchInlineSnapshot.call(this, received, ...args); + }, + toMatchUserInlineSnapshot(received, ...args) { + return toMatchInlineSnapshot.call( + this, + received, + { + createdAt: expect.any(Date), + id: expect.any(Number), + }, + ...args + ); + }, +}); +test('inline snapshots', () => { + const user = { + createdAt: new Date(), + id: Math.floor(Math.random() * 20), + name: 'LeBron James', + }; + expect(user).toMatchCustomInlineSnapshot( + { + createdAt: expect.any(Date), + id: expect.any(Number), + }, + \` + Object { + "createdAt": Any, + "id": Any, + "name": "LeBron James", + } + \` + ); + expect(user).toMatchUserInlineSnapshot(\` + Object { + "createdAt": Any, + "id": Any, + "name": "LeBron James", + } + \`); +}); + +`; + exports[`supports custom matchers: custom matchers 1`] = ` const {toMatchInlineSnapshot} = require('jest-snapshot'); expect.extend({ diff --git a/e2e/__tests__/toMatchInlineSnapshot.test.ts b/e2e/__tests__/toMatchInlineSnapshot.test.ts index 9a81893981ae..a58cb03fffae 100644 --- a/e2e/__tests__/toMatchInlineSnapshot.test.ts +++ b/e2e/__tests__/toMatchInlineSnapshot.test.ts @@ -289,6 +289,50 @@ test('supports custom matchers', () => { expect(wrap(fileAfter)).toMatchSnapshot('custom matchers'); }); +test('supports custom matchers with property matcher', () => { + const filename = 'custom-matchers-with-property-matcher.test.js'; + const test = ` + const { toMatchInlineSnapshot } = require('jest-snapshot'); + expect.extend({ + toMatchCustomInlineSnapshot(received, ...args) { + return toMatchInlineSnapshot.call(this, received, ...args); + }, + toMatchUserInlineSnapshot(received, ...args) { + return toMatchInlineSnapshot.call( + this, + received, + { + createdAt: expect.any(Date), + id: expect.any(Number), + }, + ...args + ); + }, + }); + test('inline snapshots', () => { + const user = { + createdAt: new Date(), + id: Math.floor(Math.random() * 20), + name: 'LeBron James', + }; + expect(user).toMatchCustomInlineSnapshot({ + createdAt: expect.any(Date), + id: expect.any(Number), + }); + expect(user).toMatchUserInlineSnapshot(); + }); + `; + + writeFiles(TESTS_DIR, {[filename]: test}); + const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('2 snapshots written from 1 test suite.'); + expect(exitCode).toBe(0); + expect(wrap(fileAfter)).toMatchSnapshot( + 'custom matchers with property matcher', + ); +}); + test('multiple custom matchers and native matchers', () => { const filename = 'multiple-matchers.test.js'; const test = `