diff --git a/index.d.ts b/index.d.ts index b853db6..b33c26d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -11,7 +11,7 @@ import {Config as JestConfig} from '@jest/types'; import { CoverageMapData } from 'istanbul-lib-coverage'; import ProjectWorkspace, {ProjectWorkspaceConfig, createProjectWorkspace, LoginShell } from './build/project_workspace'; export {createProjectWorkspace, ProjectWorkspaceConfig, ProjectWorkspace, LoginShell}; - +import {SourceLocation} from '@babel/types'; export interface RunArgs { args: string[]; replace?: boolean; // default is false @@ -224,10 +224,20 @@ export interface SnapshotMetadata { content?: string; } +export interface SnapshotNode{ + name: string; + loc: SourceLocation; +} +export interface SnapshotBlock{ + node: SnapshotNode; + parents: SnapshotNode[]; +} export class Snapshot { constructor(parser?: any, customMatchers?: string[]); getMetadata(filepath: string, verbose?: boolean): SnapshotMetadata[]; getMetadataAsync(filePath: string, verbose?: boolean): Promise>; + parse(filePath: string, verbose?: boolean): SnapshotBlock[]; + getSnapshotContent(filePath: string, testFullName: string): Promise; } type FormattedTestResults = { diff --git a/package.json b/package.json index b407819..27808ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jest-editor-support", - "version": "30.2.1", + "version": "30.3.0", "repository": { "type": "git", "url": "https://github.com/jest-community/jest-editor-support" diff --git a/src/Snapshot.js b/src/Snapshot.js index a696558..0d581b4 100644 --- a/src/Snapshot.js +++ b/src/Snapshot.js @@ -78,6 +78,11 @@ const buildName: (snapshotNode: Node, parents: Array, position: number) => return utils.testNameToKey(fullName, position); }; +export interface SnapshotNode { + node: Node; + parents: Node[]; +} + export default class Snapshot { _parser: Function; @@ -100,19 +105,7 @@ export default class Snapshot { ); } - async getMetadataAsync(filePath: string, verbose: boolean = false): Promise> { - if (!this.snapshotResolver) { - await this._resolverPromise; - } - return this.getMetadata(filePath, verbose); - } - - getMetadata(filePath: string, verbose: boolean = false): Array { - if (!this.snapshotResolver) { - throw new Error('snapshotResolver is not ready yet, consider migrating to "getMetadataAsync" instead'); - } - const snapshotPath = this.snapshotResolver.resolveSnapshotPath(filePath); - + parse(filePath: string, verbose: boolean = false): SnapshotNode[] { let fileNode; try { fileNode = this._parser(filePath); @@ -123,13 +116,11 @@ export default class Snapshot { } return []; } - const state = { - found: [], - }; + const Visitors = { - Identifier(path, _state, matchers) { + Identifier(path, found, matchers) { if (matchers.indexOf(path.node.name) >= 0) { - _state.found.push({ + found.push({ node: path.node, parents: getArrayOfParents(path), }); @@ -137,23 +128,70 @@ export default class Snapshot { }, }; + const found = []; + traverse(fileNode, { enter: (path) => { const visitor = Visitors[path.node.type]; if (visitor != null) { - visitor(path, state, this._matchers); + visitor(path, found, this._matchers); } }, }); - // NOTE if no projectConfig is given the default resolver will be used + return found.map((f) => ({ + node: f.node, + parents: f.parents.filter(isValidParent), + })); + } + async _getSnapshotResolver(): Promise { + if (!this.snapshotResolver) { + await this._resolverPromise; + } + return this.snapshotResolver; + } + + /** + * look for snapshot content for the given test. + * @param {*} filePath + * @param {*} testFullName + * @param autoPosition if true (the default), it will append position ("1") to the testFullName, + * otherwise, the testFullName should include the position in it. + * @returns the content of the snapshot, if exist. otherwise undefined. + * @throws throws exception if the snapshot version mismatched or any other unexpected error. + */ + async getSnapshotContent( + filePath: string, + testFullName: string, + autoPosition: boolean = true + ): Promise { + const snapshotResolver = await this._getSnapshotResolver(); + + const snapshotPath = snapshotResolver.resolveSnapshotPath(filePath); + const snapshots = utils.getSnapshotData(snapshotPath, 'none').data; + const name = autoPosition ? `${testFullName} 1` : testFullName; + return snapshots[name]; + } + + async getMetadataAsync(filePath: string, verbose: boolean = false): Promise> { + await this._getSnapshotResolver(); + return this.getMetadata(filePath, verbose); + } + + getMetadata(filePath: string, verbose: boolean = false): Array { + if (!this.snapshotResolver) { + throw new Error('snapshotResolver is not ready yet, consider migrating to "getMetadataAsync" instead'); + } + const snapshotPath = this.snapshotResolver.resolveSnapshotPath(filePath); + const snapshotNodes = this.parse(filePath, verbose); const snapshots = utils.getSnapshotData(snapshotPath, 'none').data; + let lastParent = null; let count = 1; - return state.found.map((snapshotNode) => { - const parents = snapshotNode.parents.filter(isValidParent); + return snapshotNodes.map((snapshotNode) => { + const {parents} = snapshotNode; const innerAssertion = parents[parents.length - 1]; if (lastParent !== innerAssertion) { diff --git a/src/__tests__/fixtures/snapshots/__snapshots__/inline-and-each.example.snap b/src/__tests__/fixtures/snapshots/__snapshots__/inline-and-each.example.snap new file mode 100644 index 0000000..6777392 --- /dev/null +++ b/src/__tests__/fixtures/snapshots/__snapshots__/inline-and-each.example.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`test.each a 1`] = `a`; +exports[`test.each b 1`] = `b`; +exports[`test.each c 1`] = `c`; +exports[`1 describe with each test.each a 1`] = `1.a`; +exports[`1 describe with each test.each b 1`] = `1.b`; +exports[`1 describe with each test.each c 1`] = `1.c`; +exports[`2 describe with each test.each a 1`] = `2.a`; +exports[`2 describe with each test.each b 1`] = `2.b`; +exports[`2 describe with each test.each c 1`] = `2.c`; +exports[`3 describe with each test.each a 1`] = `3.a`; +exports[`3 describe with each test.each b 1`] = `3.b`; +exports[`3 describe with each test.each c 1`] = `3.c`; +exports[`tests with each case 1 test 1-D array each 1`] = `1 1-D`; +exports[`tests with each case 2 test 1-D array each 1`] = `2 1-D`; +exports[`tests with each case 3 test 1-D array each 1`] = `3 1-D`; +exports[`literal test 1`] = `literal test 1 content`; +exports[`literal test 2`] = `literal test 2 content`; diff --git a/src/__tests__/fixtures/snapshots/inline-and-each.example b/src/__tests__/fixtures/snapshots/inline-and-each.example new file mode 100644 index 0000000..1bdbddb --- /dev/null +++ b/src/__tests__/fixtures/snapshots/inline-and-each.example @@ -0,0 +1,35 @@ +describe('tests with each', () => { + it.each` + case|whatever + ${1}|${'a'} + $(2)|${'b'} + `('case $case: test tabled each', ({whatever}) => { + expect(whatever).toMatchSnapshot(); + expect(whatever).toMatchInlineSnapshot(); + }); + it.each([1,2,3])('case %d test 1-D array each', (n) => { + expect(n).toThrowErrorMatchingSnapshot(); + expect(n).toMatchInlineSnapshot(); + + }); +}); + +describe.each([1,2,3])('%d describe with each', (n) => { + it.each(['a', 'b', 'c'])('test.each %s', (char) => { + expect({n, char}).toMatchSnapshot(); + }); + it('a regular test', () => { + expect(n).toMatchInlineSnapshot(); + }); +}); + +it.each(['a', 'b', 'c'])('inline test.each %s', (char) => { + expect(char).toThrowErrorMatchingInlineSnapshot(); +}); +it.each(['a', 'b', 'c'])('test.each %s', (char) => { + expect(char).toMatchSnapshot(); +}); +it('regular inline test', () => { + expect(whatever).toMatchInlineSnapshot(); +}); + diff --git a/src/__tests__/snapshot.test.js b/src/__tests__/snapshot.test.js index 39d8a91..44c9f68 100644 --- a/src/__tests__/snapshot.test.js +++ b/src/__tests__/snapshot.test.js @@ -118,3 +118,59 @@ describe('when metadata parse error', () => { expect(console.warn).toHaveBeenCalled(); }); }); + +describe('parse', () => { + it('can parse and return matched nodes', () => { + const filePath = path.join(snapshotFixturePath, 'nested.example'); + const snapshotNodes = snapshotHelper.parse(filePath); + expect(snapshotNodes).toHaveLength(2); + snapshotNodes.forEach((n) => expect(n.node.name).toEqual('toMatchSnapshot')); + snapshotNodes.forEach((n) => expect(n.parents).toHaveLength(4)); + expect(snapshotNodes[0].node.loc.start).toEqual({column: 21, line: 5}); + expect(snapshotNodes[0].node.loc.end).toEqual({column: 36, line: 5}); + expect(snapshotNodes[1].node.loc.start).toEqual({column: 21, line: 6}); + expect(snapshotNodes[1].node.loc.end).toEqual({column: 36, line: 6}); + }); + it('can parse inline snapshots', () => { + const filePath = path.join(snapshotFixturePath, 'inline-and-each.example'); + + let snapshot = new Snapshot(); + let snapshotNodes = snapshot.parse(filePath); + let inlineSnapshotNodes = snapshotNodes.filter((sn) => sn.node.name === 'toMatchInlineSnapshot'); + expect(inlineSnapshotNodes).toHaveLength(0); + let inlineThrowSnapshotNodes = snapshotNodes.filter((sn) => sn.node.name === 'toThrowErrorMatchingInlineSnapshot'); + expect(inlineThrowSnapshotNodes).toHaveLength(0); + + snapshot = new Snapshot(undefined, ['toMatchInlineSnapshot', 'toThrowErrorMatchingInlineSnapshot']); + snapshotNodes = snapshot.parse(filePath); + inlineSnapshotNodes = snapshotNodes.filter((sn) => sn.node.name === 'toMatchInlineSnapshot'); + expect(inlineSnapshotNodes).toHaveLength(4); + inlineThrowSnapshotNodes = snapshotNodes.filter((sn) => sn.node.name === 'toThrowErrorMatchingInlineSnapshot'); + expect(inlineThrowSnapshotNodes).toHaveLength(1); + }); +}); +describe('getSnapshotContent', () => { + it.each` + testName | expected + ${'regular inline test'} | ${undefined} + ${'test.each %s'} | ${undefined} + ${'test.each a'} | ${'a'} + ${'1 describe with each test.each a'} | ${'1.a'} + ${'2 describe with each test.each b'} | ${'2.b'} + ${'tests with each case %d test 1-D array each'} | ${undefined} + ${'tests with each case 3 test 1-D array each'} | ${'3 1-D'} + `('', async ({testName, expected}) => { + const filePath = path.join(snapshotFixturePath, 'inline-and-each.example'); + const snapshot = new Snapshot(undefined, ['toMatchInlineSnapshot', 'toThrowErrorMatchingInlineSnapshot']); + const content = await snapshot.getSnapshotContent(filePath, testName); + expect(content).toEqual(expected); + }); + it('can take literal snapshot name', async () => { + const filePath = path.join(snapshotFixturePath, 'inline-and-each.example'); + const snapshot = new Snapshot(undefined, ['toMatchInlineSnapshot', 'toThrowErrorMatchingInlineSnapshot']); + let content = await snapshot.getSnapshotContent(filePath, `literal test 2`); + expect(content).toBeUndefined(); + content = await snapshot.getSnapshotContent(filePath, `literal test 2`, false); + expect(content).toEqual('literal test 2 content'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 7c00b2f..847484a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1731,9 +1731,9 @@ camelcase@^6.2.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001254: - version "1.0.30001258" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001258.tgz#b604eed80cc54a578e4bf5a02ae3ed49f869d252" - integrity sha512-RBByOG6xWXUp0CR2/WU2amXz3stjKpSl5J1xU49F1n2OxD//uBZO4wCKUiG+QMGf7CHGfDDcqoKriomoGVxTeA== + version "1.0.30001431" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz" + integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ== chalk@^2.0.0: version "2.4.2"