From bad4ae614a5ba0aa3d2c7ccb4fc6a1027071e8fb Mon Sep 17 00:00:00 2001 From: connectdotz Date: Tue, 28 May 2024 22:37:19 -0400 Subject: [PATCH 1/6] split some "jest-snapshot" utility functions to its own package "@jest/snapshot-utils" --- e2e/__tests__/findRelatedFiles.test.ts | 12 +- e2e/__tests__/multiProjectRunner.test.ts | 14 +- e2e/__tests__/unexpectedToken.test.ts | 6 +- packages/jest-snapshot-utils/package.json | 37 ++++ .../src/__tests__/utils.test.ts | 179 ++++++++++++++++++ packages/jest-snapshot-utils/src/index.ts | 2 + packages/jest-snapshot-utils/src/types.ts | 1 + packages/jest-snapshot-utils/src/utils.ts | 156 +++++++++++++++ packages/jest-snapshot-utils/tsconfig.json | 12 ++ packages/jest-snapshot/package.json | 1 + packages/jest-snapshot/src/State.ts | 14 +- .../jest-snapshot/src/__tests__/utils.test.ts | 167 ---------------- packages/jest-snapshot/src/index.ts | 3 +- packages/jest-snapshot/src/types.ts | 2 - packages/jest-snapshot/src/utils.ts | 150 +-------------- packages/jest-snapshot/tsconfig.json | 1 + yarn.lock | 14 ++ 17 files changed, 431 insertions(+), 340 deletions(-) create mode 100644 packages/jest-snapshot-utils/package.json create mode 100644 packages/jest-snapshot-utils/src/__tests__/utils.test.ts create mode 100644 packages/jest-snapshot-utils/src/index.ts create mode 100644 packages/jest-snapshot-utils/src/types.ts create mode 100644 packages/jest-snapshot-utils/src/utils.ts create mode 100644 packages/jest-snapshot-utils/tsconfig.json diff --git a/e2e/__tests__/findRelatedFiles.test.ts b/e2e/__tests__/findRelatedFiles.test.ts index 525fadf59394..f1ba71fe4c61 100644 --- a/e2e/__tests__/findRelatedFiles.test.ts +++ b/e2e/__tests__/findRelatedFiles.test.ts @@ -18,7 +18,7 @@ afterEach(() => cleanup(DIR)); describe('--findRelatedTests flag', () => { test('runs tests related to filename', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', '__tests__/test.test.js': ` const a = require('../a'); test('a', () => {}); @@ -44,7 +44,7 @@ describe('--findRelatedTests flag', () => { } writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', '__tests__/test.test.js': ` const a = require('../a'); test('a', () => {}); @@ -65,7 +65,7 @@ describe('--findRelatedTests flag', () => { test('runs tests related to filename with a custom dependency extractor', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', '__tests__/test-skip-deps.test.js': ` const dynamicImport = path => Promise.resolve(require(path)); test('a', () => dynamicImport('../a').then(a => { @@ -118,7 +118,7 @@ describe('--findRelatedTests flag', () => { test('runs tests related to filename with a custom dependency extractor written in ESM', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', '__tests__/test-skip-deps.test.js': ` const dynamicImport = path => Promise.resolve(require(path)); test('a', () => dynamicImport('../a').then(a => { @@ -168,7 +168,7 @@ describe('--findRelatedTests flag', () => { test('generates coverage report for filename', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', '__tests__/a.test.js': ` require('../a'); require('../b'); @@ -219,7 +219,7 @@ describe('--findRelatedTests flag', () => { test('coverage configuration is applied correctly', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', '__tests__/a.test.js': ` require('../a'); test('a', () => expect(1).toBe(1)); diff --git a/e2e/__tests__/multiProjectRunner.test.ts b/e2e/__tests__/multiProjectRunner.test.ts index 84a4e79dd928..0ec2750c5959 100644 --- a/e2e/__tests__/multiProjectRunner.test.ts +++ b/e2e/__tests__/multiProjectRunner.test.ts @@ -19,7 +19,7 @@ afterEach(() => cleanup(DIR)); test("--listTests doesn't duplicate the test files", () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', '/project1.js': "module.exports = {rootDir: './', displayName: 'BACKEND'}", '/project2.js': "module.exports = {rootDir: './', displayName: 'BACKEND'}", '__tests__/inBothProjectsTest.js': "test('test', () => {});", @@ -35,7 +35,7 @@ test("--listTests doesn't duplicate the test files", () => { test('can pass projects or global config', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', 'base_config.js': ` module.exports = { haste: { @@ -132,7 +132,7 @@ test('can pass projects or global config', () => { test('"No tests found" message for projects', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', 'package.json': '{}', 'project1/__tests__/file1.test.js': ` const file1 = require('../file1'); @@ -336,7 +336,7 @@ test('allows a single project', () => { test('resolves projects and their properly', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', 'package.json': JSON.stringify({ jest: { projects: [ @@ -436,7 +436,7 @@ test('resolves projects and their properly', () => { test('Does transform files with the corresponding project transformer', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', 'file.js': SAMPLE_FILE_CONTENT, 'package.json': '{}', 'project1/__tests__/project1.test.js': ` @@ -487,7 +487,7 @@ test('Does transform files with the corresponding project transformer', () => { describe("doesn't bleed module file extensions resolution with multiple workers", () => { test('external config files', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', 'file.js': 'module.exports = "file1"', 'file.p2.js': 'module.exports = "file2"', 'package.json': '{}', @@ -537,7 +537,7 @@ describe("doesn't bleed module file extensions resolution with multiple workers" test('inline config files', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', 'file.js': 'module.exports = "file1"', 'file.p2.js': 'module.exports = "file2"', 'package.json': JSON.stringify({ diff --git a/e2e/__tests__/unexpectedToken.test.ts b/e2e/__tests__/unexpectedToken.test.ts index 27c9cca6ee21..d5e3163fc78b 100644 --- a/e2e/__tests__/unexpectedToken.test.ts +++ b/e2e/__tests__/unexpectedToken.test.ts @@ -17,7 +17,7 @@ afterEach(() => cleanup(DIR)); test('triggers unexpected token error message for non-JS assets', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', 'asset.css': '.style {}', 'package.json': JSON.stringify({jest: {testEnvironment: 'node'}}), }); @@ -37,7 +37,7 @@ test('triggers unexpected token error message for non-JS assets', () => { test('triggers unexpected token error message for untranspiled node_modules', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', 'node_modules/untranspiled-module': 'import {module} from "some-module"', 'package.json': JSON.stringify({jest: {testEnvironment: 'node'}}), }); @@ -59,7 +59,7 @@ test('triggers unexpected token error message for untranspiled node_modules', () test('does not trigger unexpected token error message for regular syntax errors', () => { writeFiles(DIR, { - '.watchmanconfig': '', + '.watchmanconfig': '{}', 'faulty.js': 'import {module from "some-module"', 'faulty2.js': 'const name = {first: "Name" second: "Second"}', 'package.json': JSON.stringify({jest: {testEnvironment: 'node'}}), diff --git a/packages/jest-snapshot-utils/package.json b/packages/jest-snapshot-utils/package.json new file mode 100644 index 000000000000..cea085ddc380 --- /dev/null +++ b/packages/jest-snapshot-utils/package.json @@ -0,0 +1,37 @@ +{ + "name": "@jest/snapshot-utils", + "version": "30.0.0-alpha.4", + "repository": { + "type": "git", + "url": "https://github.com/jestjs/jest.git", + "directory": "packages/jest-snapshot-utils" + }, + "license": "MIT", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "require": "./build/index.js", + "import": "./build/index.mjs", + "default": "./build/index.js" + }, + "./package.json": "./package.json" + }, + "dependencies": { + "@jest/types": "workspace:*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "natural-compare": "^1.4.0" + }, + "devDependencies": { + "@types/graceful-fs": "^4.1.3", + "@types/natural-compare": "^1.4.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/jest-snapshot-utils/src/__tests__/utils.test.ts b/packages/jest-snapshot-utils/src/__tests__/utils.test.ts new file mode 100644 index 000000000000..62c0d6cb339d --- /dev/null +++ b/packages/jest-snapshot-utils/src/__tests__/utils.test.ts @@ -0,0 +1,179 @@ +/** + * 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. + */ + +jest.mock('graceful-fs', () => ({ + ...jest.createMockFromModule('fs'), + existsSync: jest.fn().mockReturnValue(true), +})); + +import * as path from 'path'; +import chalk = require('chalk'); +import * as fs from 'graceful-fs'; +import { + SNAPSHOT_GUIDE_LINK, + SNAPSHOT_VERSION, + SNAPSHOT_VERSION_WARNING, + getSnapshotData, + keyToTestName, + saveSnapshotFile, + testNameToKey, +} from '../utils'; + +test('keyToTestName()', () => { + expect(keyToTestName('abc cde 12')).toBe('abc cde'); + expect(keyToTestName('abc cde 12')).toBe('abc cde '); + expect(() => keyToTestName('abc cde')).toThrow( + 'Snapshot keys must end with a number.', + ); +}); + +test('testNameToKey', () => { + expect(testNameToKey('abc cde', 1)).toBe('abc cde 1'); + expect(testNameToKey('abc cde ', 12)).toBe('abc cde 12'); +}); + +test('saveSnapshotFile() works with \r\n', () => { + const filename = path.join(__dirname, 'remove-newlines.snap'); + const data = { + myKey: '
\r\n
', + }; + + saveSnapshotFile(data, filename); + expect(fs.writeFileSync).toHaveBeenCalledWith( + filename, + `// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` + + 'exports[`myKey`] = `
\n
`;\n', + ); +}); + +test('saveSnapshotFile() works with \r', () => { + const filename = path.join(__dirname, 'remove-newlines.snap'); + const data = { + myKey: '
\r
', + }; + + saveSnapshotFile(data, filename); + expect(fs.writeFileSync).toHaveBeenCalledWith( + filename, + `// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` + + 'exports[`myKey`] = `
\n
`;\n', + ); +}); + +test('getSnapshotData() throws when no snapshot version', () => { + const filename = path.join(__dirname, 'old-snapshot.snap'); + jest + .mocked(fs.readFileSync) + .mockReturnValue('exports[`myKey`] = `
\n
`;\n'); + const update = 'none'; + + expect(() => getSnapshotData(filename, update)).toThrow( + chalk.red( + `${chalk.bold('Outdated snapshot')}: No snapshot header found. ` + + 'Jest 19 introduced versioned snapshots to ensure all developers on ' + + 'a project are using the same version of Jest. ' + + 'Please update all snapshots during this upgrade of Jest.\n\n', + ) + SNAPSHOT_VERSION_WARNING, + ); +}); + +test('getSnapshotData() throws for older snapshot version', () => { + const filename = path.join(__dirname, 'old-snapshot.snap'); + jest + .mocked(fs.readFileSync) + .mockReturnValue( + `// Jest Snapshot v0.99, ${SNAPSHOT_GUIDE_LINK}\n\n` + + 'exports[`myKey`] = `
\n
`;\n', + ); + const update = 'none'; + + expect(() => getSnapshotData(filename, update)).toThrow( + `${chalk.red( + `${chalk.red.bold('Outdated snapshot')}: The version of the snapshot ` + + 'file associated with this test is outdated. The snapshot file ' + + 'version ensures that all developers on a project are using ' + + 'the same version of Jest. ' + + 'Please update all snapshots during this upgrade of Jest.', + )}\n\nExpected: v${SNAPSHOT_VERSION}\n` + + `Received: v0.99\n\n${SNAPSHOT_VERSION_WARNING}`, + ); +}); + +test('getSnapshotData() throws for newer snapshot version', () => { + const filename = path.join(__dirname, 'old-snapshot.snap'); + jest + .mocked(fs.readFileSync) + .mockReturnValue( + `// Jest Snapshot v2, ${SNAPSHOT_GUIDE_LINK}\n\n` + + 'exports[`myKey`] = `
\n
`;\n', + ); + const update = 'none'; + + expect(() => getSnapshotData(filename, update)).toThrow( + `${chalk.red( + `${chalk.red.bold('Outdated Jest version')}: The version of this ` + + 'snapshot file indicates that this project is meant to be used ' + + 'with a newer version of Jest. ' + + 'The snapshot file version ensures that all developers on a project ' + + 'are using the same version of Jest. ' + + 'Please update your version of Jest and re-run the tests.', + )}\n\nExpected: v${SNAPSHOT_VERSION}\nReceived: v2`, + ); +}); + +test('getSnapshotData() does not throw for when updating', () => { + const filename = path.join(__dirname, 'old-snapshot.snap'); + jest + .mocked(fs.readFileSync) + .mockReturnValue('exports[`myKey`] = `
\n
`;\n'); + const update = 'all'; + + expect(() => getSnapshotData(filename, update)).not.toThrow(); +}); + +test('getSnapshotData() marks invalid snapshot dirty when updating', () => { + const filename = path.join(__dirname, 'old-snapshot.snap'); + jest + .mocked(fs.readFileSync) + .mockReturnValue('exports[`myKey`] = `
\n
`;\n'); + const update = 'all'; + + expect(getSnapshotData(filename, update)).toMatchObject({dirty: true}); +}); + +test('getSnapshotData() marks valid snapshot not dirty when updating', () => { + const filename = path.join(__dirname, 'old-snapshot.snap'); + jest + .mocked(fs.readFileSync) + .mockReturnValue( + `// Jest Snapshot v${SNAPSHOT_VERSION}, ${SNAPSHOT_GUIDE_LINK}\n\n` + + 'exports[`myKey`] = `
\n
`;\n', + ); + const update = 'all'; + + expect(getSnapshotData(filename, update)).toMatchObject({dirty: false}); +}); + +test('escaping', () => { + const filename = path.join(__dirname, 'escaping.snap'); + const data = '"\'\\'; + const writeFileSync = jest.mocked(fs.writeFileSync); + + writeFileSync.mockReset(); + saveSnapshotFile({key: data}, filename); + const writtenData = writeFileSync.mock.calls[0][1]; + expect(writtenData).toBe( + `// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` + + 'exports[`key`] = `"\'\\\\`;\n', + ); + + // eslint-disable-next-line no-eval + const readData = eval(`var exports = {}; ${writtenData} exports`); + expect(readData).toEqual({key: data}); + const snapshotData = readData.key; + expect(data).toEqual(snapshotData); +}); diff --git a/packages/jest-snapshot-utils/src/index.ts b/packages/jest-snapshot-utils/src/index.ts new file mode 100644 index 000000000000..f0c4eaa5e075 --- /dev/null +++ b/packages/jest-snapshot-utils/src/index.ts @@ -0,0 +1,2 @@ +export * from './utils'; +export * from './types'; diff --git a/packages/jest-snapshot-utils/src/types.ts b/packages/jest-snapshot-utils/src/types.ts new file mode 100644 index 000000000000..434c4e4d66b1 --- /dev/null +++ b/packages/jest-snapshot-utils/src/types.ts @@ -0,0 +1 @@ +export type SnapshotData = Record; diff --git a/packages/jest-snapshot-utils/src/utils.ts b/packages/jest-snapshot-utils/src/utils.ts new file mode 100644 index 000000000000..2c81b4a9c52f --- /dev/null +++ b/packages/jest-snapshot-utils/src/utils.ts @@ -0,0 +1,156 @@ +/** + * 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 chalk = require('chalk'); +import * as fs from 'graceful-fs'; +import naturalCompare = require('natural-compare'); +import type {Config} from '@jest/types'; +import type {SnapshotData} from './types'; + +export const SNAPSHOT_VERSION = '1'; +const SNAPSHOT_VERSION_REGEXP = /^\/\/ Jest Snapshot v(.+),/; +export const SNAPSHOT_GUIDE_LINK = 'https://goo.gl/fbAQLP'; +export const SNAPSHOT_VERSION_WARNING = chalk.yellow( + `${chalk.bold('Warning')}: Before you upgrade snapshots, ` + + 'we recommend that you revert any local changes to tests or other code, ' + + 'to ensure that you do not store invalid state.', +); + +const writeSnapshotVersion = () => + `// Jest Snapshot v${SNAPSHOT_VERSION}, ${SNAPSHOT_GUIDE_LINK}`; + +const validateSnapshotVersion = (snapshotContents: string) => { + const versionTest = SNAPSHOT_VERSION_REGEXP.exec(snapshotContents); + const version = versionTest && versionTest[1]; + + if (!version) { + return new Error( + chalk.red( + `${chalk.bold('Outdated snapshot')}: No snapshot header found. ` + + 'Jest 19 introduced versioned snapshots to ensure all developers ' + + 'on a project are using the same version of Jest. ' + + 'Please update all snapshots during this upgrade of Jest.\n\n', + ) + SNAPSHOT_VERSION_WARNING, + ); + } + + if (version < SNAPSHOT_VERSION) { + return new Error( + // eslint-disable-next-line prefer-template + chalk.red( + `${chalk.red.bold('Outdated snapshot')}: The version of the snapshot ` + + 'file associated with this test is outdated. The snapshot file ' + + 'version ensures that all developers on a project are using ' + + 'the same version of Jest. ' + + 'Please update all snapshots during this upgrade of Jest.', + ) + + '\n\n' + + `Expected: v${SNAPSHOT_VERSION}\n` + + `Received: v${version}\n\n` + + SNAPSHOT_VERSION_WARNING, + ); + } + + if (version > SNAPSHOT_VERSION) { + return new Error( + // eslint-disable-next-line prefer-template + chalk.red( + `${chalk.red.bold('Outdated Jest version')}: The version of this ` + + 'snapshot file indicates that this project is meant to be used ' + + 'with a newer version of Jest. The snapshot file version ensures ' + + 'that all developers on a project are using the same version of ' + + 'Jest. Please update your version of Jest and re-run the tests.', + ) + + '\n\n' + + `Expected: v${SNAPSHOT_VERSION}\n` + + `Received: v${version}`, + ); + } + + return null; +}; + +export const testNameToKey = (testName: string, count: number): string => + `${testName} ${count}`; + +export const keyToTestName = (key: string): string => { + if (!/ \d+$/.test(key)) { + throw new Error('Snapshot keys must end with a number.'); + } + + return key.replace(/ \d+$/, ''); +}; + +export const getSnapshotData = ( + snapshotPath: string, + update: Config.SnapshotUpdateState, +): { + data: SnapshotData; + dirty: boolean; +} => { + const data = Object.create(null); + let snapshotContents = ''; + let dirty = false; + + if (fs.existsSync(snapshotPath)) { + try { + snapshotContents = fs.readFileSync(snapshotPath, 'utf8'); + // eslint-disable-next-line no-new-func + const populate = new Function('exports', snapshotContents); + populate(data); + } catch {} + } + + const validationResult = validateSnapshotVersion(snapshotContents); + const isInvalid = snapshotContents && validationResult; + + if (update === 'none' && isInvalid) { + throw validationResult; + } + + if ((update === 'all' || update === 'new') && isInvalid) { + dirty = true; + } + + return {data, dirty}; +}; + +export const escapeBacktickString = (str: string): string => + str.replaceAll(/`|\\|\${/g, '\\$&'); + +const printBacktickString = (str: string): string => + `\`${escapeBacktickString(str)}\``; + +export const ensureDirectoryExists = (filePath: string): void => { + try { + fs.mkdirSync(path.dirname(filePath), {recursive: true}); + } catch {} +}; + +export const normalizeNewlines = (string: string): string => + string.replaceAll(/\r\n|\r/g, '\n'); + +export const saveSnapshotFile = ( + snapshotData: SnapshotData, + snapshotPath: string, +): void => { + const snapshots = Object.keys(snapshotData) + .sort(naturalCompare) + .map( + key => + `exports[${printBacktickString(key)}] = ${printBacktickString( + normalizeNewlines(snapshotData[key]), + )};`, + ); + + ensureDirectoryExists(snapshotPath); + fs.writeFileSync( + snapshotPath, + `${writeSnapshotVersion()}\n\n${snapshots.join('\n\n')}\n`, + ); +}; diff --git a/packages/jest-snapshot-utils/tsconfig.json b/packages/jest-snapshot-utils/tsconfig.json new file mode 100644 index 000000000000..1329dad998ae --- /dev/null +++ b/packages/jest-snapshot-utils/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build" + }, + "include": ["./src/**/*"], + "exclude": ["./**/__mocks__/**/*", "./**/__tests__/**/*"], + "references": [ + {"path": "../jest-types"} + ] +} \ No newline at end of file diff --git a/packages/jest-snapshot/package.json b/packages/jest-snapshot/package.json index 3cf69bb4c4c0..b74a7de29d66 100644 --- a/packages/jest-snapshot/package.json +++ b/packages/jest-snapshot/package.json @@ -25,6 +25,7 @@ "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "workspace:*", + "@jest/snapshot-utils": "workspace:*", "@jest/transform": "workspace:*", "@jest/types": "workspace:*", "babel-preset-current-node-syntax": "^1.0.0", diff --git a/packages/jest-snapshot/src/State.ts b/packages/jest-snapshot/src/State.ts index 3ecd8cef1dec..6ed107b0fc2a 100644 --- a/packages/jest-snapshot/src/State.ts +++ b/packages/jest-snapshot/src/State.ts @@ -6,21 +6,23 @@ */ import * as fs from 'graceful-fs'; +import { + type SnapshotData, + getSnapshotData, + keyToTestName, + saveSnapshotFile, + testNameToKey, +} from '@jest/snapshot-utils'; import type {Config} from '@jest/types'; import {getStackTraceLines, getTopFrame} from 'jest-message-util'; import {saveInlineSnapshots} from './InlineSnapshots'; -import type {InlineSnapshot, SnapshotData, SnapshotFormat} from './types'; +import type {InlineSnapshot, SnapshotFormat} from './types'; import { addExtraLineBreaks, - getSnapshotData, - keyToTestName, removeExtraLineBreaks, removeLinesBeforeExternalMatcherTrap, - saveSnapshotFile, serialize, - testNameToKey, } from './utils'; - export type SnapshotStateOptions = { readonly updateSnapshot: Config.SnapshotUpdateState; readonly prettierPath?: string | null; diff --git a/packages/jest-snapshot/src/__tests__/utils.test.ts b/packages/jest-snapshot/src/__tests__/utils.test.ts index 36f4d73dfdf2..4dab9c2ee947 100644 --- a/packages/jest-snapshot/src/__tests__/utils.test.ts +++ b/packages/jest-snapshot/src/__tests__/utils.test.ts @@ -11,181 +11,14 @@ jest.mock('graceful-fs', () => ({ })); import {strict as assert} from 'assert'; -import * as path from 'path'; -import chalk = require('chalk'); -import * as fs from 'graceful-fs'; import { - SNAPSHOT_GUIDE_LINK, - SNAPSHOT_VERSION, - SNAPSHOT_VERSION_WARNING, addExtraLineBreaks, deepMerge, - getSnapshotData, - keyToTestName, removeExtraLineBreaks, removeLinesBeforeExternalMatcherTrap, - saveSnapshotFile, serialize, - testNameToKey, } from '../utils'; -test('keyToTestName()', () => { - expect(keyToTestName('abc cde 12')).toBe('abc cde'); - expect(keyToTestName('abc cde 12')).toBe('abc cde '); - expect(() => keyToTestName('abc cde')).toThrow( - 'Snapshot keys must end with a number.', - ); -}); - -test('testNameToKey', () => { - expect(testNameToKey('abc cde', 1)).toBe('abc cde 1'); - expect(testNameToKey('abc cde ', 12)).toBe('abc cde 12'); -}); - -test('saveSnapshotFile() works with \r\n', () => { - const filename = path.join(__dirname, 'remove-newlines.snap'); - const data = { - myKey: '
\r\n
', - }; - - saveSnapshotFile(data, filename); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, - `// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` + - 'exports[`myKey`] = `
\n
`;\n', - ); -}); - -test('saveSnapshotFile() works with \r', () => { - const filename = path.join(__dirname, 'remove-newlines.snap'); - const data = { - myKey: '
\r
', - }; - - saveSnapshotFile(data, filename); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, - `// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` + - 'exports[`myKey`] = `
\n
`;\n', - ); -}); - -test('getSnapshotData() throws when no snapshot version', () => { - const filename = path.join(__dirname, 'old-snapshot.snap'); - jest - .mocked(fs.readFileSync) - .mockReturnValue('exports[`myKey`] = `
\n
`;\n'); - const update = 'none'; - - expect(() => getSnapshotData(filename, update)).toThrow( - chalk.red( - `${chalk.bold('Outdated snapshot')}: No snapshot header found. ` + - 'Jest 19 introduced versioned snapshots to ensure all developers on ' + - 'a project are using the same version of Jest. ' + - 'Please update all snapshots during this upgrade of Jest.\n\n', - ) + SNAPSHOT_VERSION_WARNING, - ); -}); - -test('getSnapshotData() throws for older snapshot version', () => { - const filename = path.join(__dirname, 'old-snapshot.snap'); - jest - .mocked(fs.readFileSync) - .mockReturnValue( - `// Jest Snapshot v0.99, ${SNAPSHOT_GUIDE_LINK}\n\n` + - 'exports[`myKey`] = `
\n
`;\n', - ); - const update = 'none'; - - expect(() => getSnapshotData(filename, update)).toThrow( - `${chalk.red( - `${chalk.red.bold('Outdated snapshot')}: The version of the snapshot ` + - 'file associated with this test is outdated. The snapshot file ' + - 'version ensures that all developers on a project are using ' + - 'the same version of Jest. ' + - 'Please update all snapshots during this upgrade of Jest.', - )}\n\nExpected: v${SNAPSHOT_VERSION}\n` + - `Received: v0.99\n\n${SNAPSHOT_VERSION_WARNING}`, - ); -}); - -test('getSnapshotData() throws for newer snapshot version', () => { - const filename = path.join(__dirname, 'old-snapshot.snap'); - jest - .mocked(fs.readFileSync) - .mockReturnValue( - `// Jest Snapshot v2, ${SNAPSHOT_GUIDE_LINK}\n\n` + - 'exports[`myKey`] = `
\n
`;\n', - ); - const update = 'none'; - - expect(() => getSnapshotData(filename, update)).toThrow( - `${chalk.red( - `${chalk.red.bold('Outdated Jest version')}: The version of this ` + - 'snapshot file indicates that this project is meant to be used ' + - 'with a newer version of Jest. ' + - 'The snapshot file version ensures that all developers on a project ' + - 'are using the same version of Jest. ' + - 'Please update your version of Jest and re-run the tests.', - )}\n\nExpected: v${SNAPSHOT_VERSION}\nReceived: v2`, - ); -}); - -test('getSnapshotData() does not throw for when updating', () => { - const filename = path.join(__dirname, 'old-snapshot.snap'); - jest - .mocked(fs.readFileSync) - .mockReturnValue('exports[`myKey`] = `
\n
`;\n'); - const update = 'all'; - - expect(() => getSnapshotData(filename, update)).not.toThrow(); -}); - -test('getSnapshotData() marks invalid snapshot dirty when updating', () => { - const filename = path.join(__dirname, 'old-snapshot.snap'); - jest - .mocked(fs.readFileSync) - .mockReturnValue('exports[`myKey`] = `
\n
`;\n'); - const update = 'all'; - - expect(getSnapshotData(filename, update)).toMatchObject({dirty: true}); -}); - -test('getSnapshotData() marks valid snapshot not dirty when updating', () => { - const filename = path.join(__dirname, 'old-snapshot.snap'); - jest - .mocked(fs.readFileSync) - .mockReturnValue( - `// Jest Snapshot v${SNAPSHOT_VERSION}, ${SNAPSHOT_GUIDE_LINK}\n\n` + - 'exports[`myKey`] = `
\n
`;\n', - ); - const update = 'all'; - - expect(getSnapshotData(filename, update)).toMatchObject({dirty: false}); -}); - -test('escaping', () => { - const filename = path.join(__dirname, 'escaping.snap'); - const data = '"\'\\'; - const writeFileSync = jest.mocked(fs.writeFileSync); - - writeFileSync.mockReset(); - saveSnapshotFile({key: data}, filename); - const writtenData = writeFileSync.mock.calls[0][1]; - expect(writtenData).toBe( - `// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` + - 'exports[`key`] = `"\'\\\\`;\n', - ); - - // @ts-expect-error: used in `eval` - const exports = {}; - // eslint-disable-next-line no-eval - const readData = eval(`var exports = {}; ${writtenData} exports`); - expect(readData).toEqual({key: data}); - const snapshotData = readData.key; - expect(data).toEqual(snapshotData); -}); - test('serialize handles \\r\\n', () => { const data = '
\r\n
'; const serializedData = serialize(data); diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index b6859cd3605e..5cd1f757303c 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -7,6 +7,7 @@ import {types} from 'util'; import * as fs from 'graceful-fs'; +import {escapeBacktickString} from '@jest/snapshot-utils'; import type {Config} from '@jest/types'; import type {MatcherFunctionWithContext} from 'expect'; import { @@ -32,7 +33,7 @@ import { printSnapshotAndReceived, } from './printSnapshot'; import type {Context, FileSystem, MatchSnapshotConfig} from './types'; -import {deepMerge, escapeBacktickString, serialize} from './utils'; +import {deepMerge, serialize} from './utils'; export {addSerializer, getSerializers} from './plugins'; export { diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index cf669b8be5c9..03eaec78c849 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -34,8 +34,6 @@ export type MatchSnapshotConfig = { received: any; }; -export type SnapshotData = Record; - export interface SnapshotMatchers, T> { /** * This ensures that a value matches the most recent snapshot with property matchers. diff --git a/packages/jest-snapshot/src/utils.ts b/packages/jest-snapshot/src/utils.ts index 817a7ef52995..347f4059adef 100644 --- a/packages/jest-snapshot/src/utils.ts +++ b/packages/jest-snapshot/src/utils.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import * as path from 'path'; import type {ParseResult, PluginItem} from '@babel/core'; import type { File, @@ -14,129 +13,19 @@ import type { TemplateLiteral, TraversalAncestors, } from '@babel/types'; -import chalk = require('chalk'); import * as fs from 'graceful-fs'; -import naturalCompare = require('natural-compare'); -import type {Config} from '@jest/types'; +import {escapeBacktickString, normalizeNewlines} from '@jest/snapshot-utils'; import { type OptionsReceived as PrettyFormatOptions, format as prettyFormat, } from 'pretty-format'; import {getSerializers} from './plugins'; -import type {InlineSnapshot, SnapshotData} from './types'; - -export const SNAPSHOT_VERSION = '1'; -const SNAPSHOT_VERSION_REGEXP = /^\/\/ Jest Snapshot v(.+),/; -export const SNAPSHOT_GUIDE_LINK = 'https://goo.gl/fbAQLP'; -export const SNAPSHOT_VERSION_WARNING = chalk.yellow( - `${chalk.bold('Warning')}: Before you upgrade snapshots, ` + - 'we recommend that you revert any local changes to tests or other code, ' + - 'to ensure that you do not store invalid state.', -); - -const writeSnapshotVersion = () => - `// Jest Snapshot v${SNAPSHOT_VERSION}, ${SNAPSHOT_GUIDE_LINK}`; - -const validateSnapshotVersion = (snapshotContents: string) => { - const versionTest = SNAPSHOT_VERSION_REGEXP.exec(snapshotContents); - const version = versionTest && versionTest[1]; - - if (!version) { - return new Error( - chalk.red( - `${chalk.bold('Outdated snapshot')}: No snapshot header found. ` + - 'Jest 19 introduced versioned snapshots to ensure all developers ' + - 'on a project are using the same version of Jest. ' + - 'Please update all snapshots during this upgrade of Jest.\n\n', - ) + SNAPSHOT_VERSION_WARNING, - ); - } - - if (version < SNAPSHOT_VERSION) { - return new Error( - // eslint-disable-next-line prefer-template - chalk.red( - `${chalk.red.bold('Outdated snapshot')}: The version of the snapshot ` + - 'file associated with this test is outdated. The snapshot file ' + - 'version ensures that all developers on a project are using ' + - 'the same version of Jest. ' + - 'Please update all snapshots during this upgrade of Jest.', - ) + - '\n\n' + - `Expected: v${SNAPSHOT_VERSION}\n` + - `Received: v${version}\n\n` + - SNAPSHOT_VERSION_WARNING, - ); - } - - if (version > SNAPSHOT_VERSION) { - return new Error( - // eslint-disable-next-line prefer-template - chalk.red( - `${chalk.red.bold('Outdated Jest version')}: The version of this ` + - 'snapshot file indicates that this project is meant to be used ' + - 'with a newer version of Jest. The snapshot file version ensures ' + - 'that all developers on a project are using the same version of ' + - 'Jest. Please update your version of Jest and re-run the tests.', - ) + - '\n\n' + - `Expected: v${SNAPSHOT_VERSION}\n` + - `Received: v${version}`, - ); - } - - return null; -}; +import type {InlineSnapshot} from './types'; function isObject(item: unknown): boolean { return item != null && typeof item === 'object' && !Array.isArray(item); } -export const testNameToKey = (testName: string, count: number): string => - `${testName} ${count}`; - -export const keyToTestName = (key: string): string => { - if (!/ \d+$/.test(key)) { - throw new Error('Snapshot keys must end with a number.'); - } - - return key.replace(/ \d+$/, ''); -}; - -export const getSnapshotData = ( - snapshotPath: string, - update: Config.SnapshotUpdateState, -): { - data: SnapshotData; - dirty: boolean; -} => { - const data = Object.create(null); - let snapshotContents = ''; - let dirty = false; - - if (fs.existsSync(snapshotPath)) { - try { - snapshotContents = fs.readFileSync(snapshotPath, 'utf8'); - // eslint-disable-next-line no-new-func - const populate = new Function('exports', snapshotContents); - populate(data); - } catch {} - } - - const validationResult = validateSnapshotVersion(snapshotContents); - const isInvalid = snapshotContents && validationResult; - - if (update === 'none' && isInvalid) { - throw validationResult; - } - - if ((update === 'all' || update === 'new') && isInvalid) { - dirty = true; - } - - return {data, dirty}; -}; - // Add extra line breaks at beginning and end of multiline snapshot // to make the content easier to read. export const addExtraLineBreaks = (string: string): string => @@ -194,41 +83,6 @@ export const minify = (val: unknown): string => export const deserializeString = (stringified: string): string => stringified.slice(1, -1).replaceAll(/\\("|\\)/g, '$1'); -export const escapeBacktickString = (str: string): string => - str.replaceAll(/`|\\|\${/g, '\\$&'); - -const printBacktickString = (str: string): string => - `\`${escapeBacktickString(str)}\``; - -export const ensureDirectoryExists = (filePath: string): void => { - try { - fs.mkdirSync(path.dirname(filePath), {recursive: true}); - } catch {} -}; - -const normalizeNewlines = (string: string) => - string.replaceAll(/\r\n|\r/g, '\n'); - -export const saveSnapshotFile = ( - snapshotData: SnapshotData, - snapshotPath: string, -): void => { - const snapshots = Object.keys(snapshotData) - .sort(naturalCompare) - .map( - key => - `exports[${printBacktickString(key)}] = ${printBacktickString( - normalizeNewlines(snapshotData[key]), - )};`, - ); - - ensureDirectoryExists(snapshotPath); - fs.writeFileSync( - snapshotPath, - `${writeSnapshotVersion()}\n\n${snapshots.join('\n\n')}\n`, - ); -}; - const isAnyOrAnything = (input: object) => '$$typeof' in input && input.$$typeof === Symbol.for('jest.asymmetricMatcher') && diff --git a/packages/jest-snapshot/tsconfig.json b/packages/jest-snapshot/tsconfig.json index af04ef522255..d6b02e799d81 100644 --- a/packages/jest-snapshot/tsconfig.json +++ b/packages/jest-snapshot/tsconfig.json @@ -13,6 +13,7 @@ {"path": "../jest-get-type"}, {"path": "../jest-matcher-utils"}, {"path": "../jest-message-util"}, + {"path": "../jest-snapshot-utils"}, {"path": "../jest-transform"}, {"path": "../jest-types"}, {"path": "../jest-util"}, diff --git a/yarn.lock b/yarn.lock index 0f90ba0eead3..3da8fe244a1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3178,6 +3178,19 @@ __metadata: languageName: unknown linkType: soft +"@jest/snapshot-utils@workspace:*, @jest/snapshot-utils@workspace:packages/jest-snapshot-utils": + version: 0.0.0-use.local + resolution: "@jest/snapshot-utils@workspace:packages/jest-snapshot-utils" + dependencies: + "@jest/types": "workspace:*" + "@types/graceful-fs": ^4.1.3 + "@types/natural-compare": ^1.4.0 + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + natural-compare: ^1.4.0 + languageName: unknown + linkType: soft + "@jest/source-map@workspace:*, @jest/source-map@workspace:packages/jest-source-map": version: 0.0.0-use.local resolution: "@jest/source-map@workspace:packages/jest-source-map" @@ -13178,6 +13191,7 @@ __metadata: "@babel/preset-react": ^7.12.1 "@babel/types": ^7.3.3 "@jest/expect-utils": "workspace:*" + "@jest/snapshot-utils": "workspace:*" "@jest/test-utils": "workspace:*" "@jest/transform": "workspace:*" "@jest/types": "workspace:*" From 03f2144b7dc523696eb245324ad7b5499ba16759 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Tue, 28 May 2024 23:05:52 -0400 Subject: [PATCH 2/6] fix lint issue --- packages/jest-snapshot-utils/tsconfig.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/jest-snapshot-utils/tsconfig.json b/packages/jest-snapshot-utils/tsconfig.json index 1329dad998ae..04d46f86078f 100644 --- a/packages/jest-snapshot-utils/tsconfig.json +++ b/packages/jest-snapshot-utils/tsconfig.json @@ -6,7 +6,5 @@ }, "include": ["./src/**/*"], "exclude": ["./**/__mocks__/**/*", "./**/__tests__/**/*"], - "references": [ - {"path": "../jest-types"} - ] -} \ No newline at end of file + "references": [{"path": "../jest-types"}] +} From 1b79a4f58647a02fbf71b7bac8976e18af842510 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Tue, 28 May 2024 23:12:32 -0400 Subject: [PATCH 3/6] address another lint issue --- scripts/lintTs.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/lintTs.mjs b/scripts/lintTs.mjs index 59dc3fa48028..41750fdf5bed 100644 --- a/scripts/lintTs.mjs +++ b/scripts/lintTs.mjs @@ -58,6 +58,7 @@ const packagesNotToTest = [ 'jest-runner', 'jest-runtime', 'jest-snapshot', + 'jest-snapshot-utils', 'jest-util', 'jest-validate', 'jest-worker', From 71cfa0dbeea206e5caa7f61a4f86c0652c97be7a Mon Sep 17 00:00:00 2001 From: connectdotz Date: Tue, 28 May 2024 23:17:08 -0400 Subject: [PATCH 4/6] added copyright headers --- packages/jest-snapshot-utils/src/index.ts | 7 +++++++ packages/jest-snapshot-utils/src/types.ts | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/packages/jest-snapshot-utils/src/index.ts b/packages/jest-snapshot-utils/src/index.ts index f0c4eaa5e075..fee9f6ef7c78 100644 --- a/packages/jest-snapshot-utils/src/index.ts +++ b/packages/jest-snapshot-utils/src/index.ts @@ -1,2 +1,9 @@ +/** + * 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. + */ + export * from './utils'; export * from './types'; diff --git a/packages/jest-snapshot-utils/src/types.ts b/packages/jest-snapshot-utils/src/types.ts index 434c4e4d66b1..0794db9d81d4 100644 --- a/packages/jest-snapshot-utils/src/types.ts +++ b/packages/jest-snapshot-utils/src/types.ts @@ -1 +1,8 @@ +/** + * 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. + */ + export type SnapshotData = Record; From 7af45364f5f20122c12ee83e245bf7ff6dd3898b Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Wed, 29 May 2024 08:19:33 +0200 Subject: [PATCH 5/6] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21d59d56382d..ccd779a5f2ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - `[@jest/types]` Improve argument type inference passed to `test` and `describe` callback functions from `each` tables ([#14920](https://github.com/jestjs/jest/pull/14920)) - `[jest-snapshot]` [**BREAKING**] Add support for [Error causes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) in snapshots ([#13965](https://github.com/facebook/jest/pull/13965)) - `[jest-snapshot]` Support Prettier 3 ([#14566](https://github.com/facebook/jest/pull/14566)) +- `[@jest/util-snapshot]` Extract utils used by tooling from `jest-snapshot` into its own package ([#15095](https://github.com/facebook/jest/pull/15095)) - `[pretty-format]` [**BREAKING**] Do not render empty string children (`''`) in React plugin ([#14470](https://github.com/facebook/jest/pull/14470)) ### Fixes From 5ae8cfdad0ea5e3d62727f124ff091db4115fb6d Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Wed, 29 May 2024 08:21:06 +0200 Subject: [PATCH 6/6] remove unused dep --- packages/jest-snapshot/package.json | 2 -- yarn.lock | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/jest-snapshot/package.json b/packages/jest-snapshot/package.json index b74a7de29d66..544f9fac9714 100644 --- a/packages/jest-snapshot/package.json +++ b/packages/jest-snapshot/package.json @@ -37,7 +37,6 @@ "jest-matcher-utils": "workspace:*", "jest-message-util": "workspace:*", "jest-util": "workspace:*", - "natural-compare": "^1.4.0", "pretty-format": "workspace:*", "semver": "^7.5.3", "synckit": "^0.9.0" @@ -48,7 +47,6 @@ "@jest/test-utils": "workspace:*", "@types/babel__core": "^7.1.14", "@types/graceful-fs": "^4.1.3", - "@types/natural-compare": "^1.4.0", "@types/prettier-v2": "npm:@types/prettier@^2.1.5", "@types/semver": "^7.1.0", "ansi-regex": "^5.0.1", diff --git a/yarn.lock b/yarn.lock index 3da8fe244a1f..08a6a2864d8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13197,7 +13197,6 @@ __metadata: "@jest/types": "workspace:*" "@types/babel__core": ^7.1.14 "@types/graceful-fs": ^4.1.3 - "@types/natural-compare": ^1.4.0 "@types/prettier-v2": "npm:@types/prettier@^2.1.5" "@types/semver": ^7.1.0 ansi-regex: ^5.0.1 @@ -13211,7 +13210,6 @@ __metadata: jest-matcher-utils: "workspace:*" jest-message-util: "workspace:*" jest-util: "workspace:*" - natural-compare: ^1.4.0 prettier: ^3.0.3 prettier-v2: "npm:prettier@^2.1.5" pretty-format: "workspace:*"