From 3d8bd214eada81a2b17514fe3930febf340a1ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 16 Feb 2017 14:42:54 +0100 Subject: [PATCH] Add snapshot version support (#2896) * Add snapshot version support * Add missing snapshots * Add link to snapshot guide, small regexp fixes * Update examples snapshots * Add parens for readability * Remove unnecessary -u flag from transform test * Update snapshot version messages to be more exhaustive * Fix error formatting * Don't write to fs if not needed --- .../__snapshots__/Intro-test.js.snap | 2 + .../__snapshots__/Clock.react-test.js.snap | 2 + .../__snapshots__/Link.react-test.js.snap | 2 + .../__snapshots__/console-test.js.snap | 2 + .../coverage_report-test.js.snap | 2 + .../__snapshots__/failures-test.js.snap | 2 + .../__snapshots__/globals-test.js.snap | 2 + .../snapshot-serializers-test.js.snap | 2 + .../__snapshots__/snapshot-test.js.snap | 2 + .../__snapshots__/stack_trace-test.js.snap | 2 + .../testNamePattern-test.js.snap | 2 + .../__snapshots__/transform-test.js.snap | 2 + .../typescript-coverage-test.js.snap | 2 + .../multiple-transformers-test.js.snap | 2 + .../generateEmptyCoverage-test.js.snap | 2 + .../watch-filename-pattern-mode-test.js.snap | 2 + .../watch-test-name-pattern-mode-test.js.snap | 2 + .../__snapshots__/watch-test.js.snap | 2 + .../formatTestNameByPattern-test.js.snap | 2 + .../__snapshots__/highlight-test.js.snap | 2 + .../__snapshots__/utils-test.js.snap | 6 +- .../__snapshots__/normalize-test.js.snap | 2 + .../__tests__/__snapshots__/diff-test.js.snap | 2 + .../__snapshots__/index-test.js.snap | 4 +- .../__snapshots__/matchers-test.js.snap | 2 + .../__snapshots__/index-test.js.snap | 2 + .../__snapshots__/extend-test.js.snap | 2 + .../__snapshots__/matchers-test.js.snap | 2 + .../__snapshots__/spyMatchers-test.js.snap | 2 + .../toThrowMatchers-test.js.snap | 2 + .../__snapshots__/messages-test.js.snap | 2 + .../__snapshots__/transform-test.js.snap | 2 + packages/jest-snapshot/src/State.js | 5 +- .../jest-snapshot/src/__tests__/utils-test.js | 97 ++++++++++++++++++- packages/jest-snapshot/src/utils.js | 84 ++++++++++++++-- .../__snapshots__/FakeTimers-test.js.snap | 2 + .../validateCLIOptions-test.js.snap | 2 + .../__snapshots__/validate-test.js.snap | 2 + .../__snapshots__/pretty-format-test.js.snap | 2 + 39 files changed, 246 insertions(+), 18 deletions(-) diff --git a/examples/react-native/__tests__/__snapshots__/Intro-test.js.snap b/examples/react-native/__tests__/__snapshots__/Intro-test.js.snap index 6edc1ae2c2fe..10ea6c84fa8d 100644 --- a/examples/react-native/__tests__/__snapshots__/Intro-test.js.snap +++ b/examples/react-native/__tests__/__snapshots__/Intro-test.js.snap @@ -1,3 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + exports[`renders correctly 1`] = ` 1482363367.071 diff --git a/examples/snapshot/__tests__/__snapshots__/Link.react-test.js.snap b/examples/snapshot/__tests__/__snapshots__/Link.react-test.js.snap index 97f6046e24f5..62627f397202 100644 --- a/examples/snapshot/__tests__/__snapshots__/Link.react-test.js.snap +++ b/examples/snapshot/__tests__/__snapshots__/Link.react-test.js.snap @@ -1,3 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + exports[`changes the class when hovered 1`] = ` \\":function(module,exports,require,__dirname,__filename,global,jest){/* istanbul ignore next */var cov_25u22311x4 = function () {var path = \\"/fruits/banana.js\\",hash = \\"04636d4ae73b4b3e24bf6fba39e08c946fd0afb5\\",global = new Function('return this')(),gcv = \\"__coverage__\\",coverageData = { path: \\"/fruits/banana.js\\", statementMap: { \\"0\\": { start: { line: 1, column: 0 }, end: { line: 1, column: 26 } } }, fnMap: {}, branchMap: {}, s: { \\"0\\": 0 }, f: {}, b: {}, _coverageSchema: \\"332fd63041d2c1bcb487cc26dd0d5f7d97098a6c\\" },coverage = global[gcv] || (global[gcv] = {});if (coverage[path] && coverage[path].hash === hash) {return coverage[path];}coverageData.hash = hash;return coverage[path] = coverageData;}();++cov_25u22311x4.s[0];module.exports = \\"banana\\"; }});" diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index 5424a410092e..d8d1753cad95 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -46,9 +46,10 @@ class SnapshotState { snapshotPath?: string, expand?: boolean, ) { - this._dirty = false; this._snapshotPath = snapshotPath || getSnapshotPath(testPath); - this._snapshotData = getSnapshotData(this._snapshotPath); + const {data, dirty} = getSnapshotData(this._snapshotPath, update); + this._snapshotData = data; + this._dirty = dirty; this._uncheckedKeys = new Set(Object.keys(this._snapshotData)); this._counters = new Map(); this._index = 0; diff --git a/packages/jest-snapshot/src/__tests__/utils-test.js b/packages/jest-snapshot/src/__tests__/utils-test.js index 5870d5a846dc..2996e798f761 100644 --- a/packages/jest-snapshot/src/__tests__/utils-test.js +++ b/packages/jest-snapshot/src/__tests__/utils-test.js @@ -9,17 +9,30 @@ 'use strict'; const { + getSnapshotData, getSnapshotPath, keyToTestName, saveSnapshotFile, testNameToKey, + SNAPSHOT_GUIDE_LINK, + SNAPSHOT_VERSION, + SNAPSHOT_VERSION_WARNING, } = require('../utils'); const fs = require('fs'); const path = require('path'); const writeFileSync = fs.writeFileSync; -beforeEach(() => fs.writeFileSync = jest.fn()); -afterEach(() => fs.writeFileSync = writeFileSync); +const readFileSync = fs.readFileSync; +beforeEach(() => { + fs.writeFileSync = jest.fn(); + fs.readFileSync = jest.fn(); +}); +afterEach(() => { + fs.writeFileSync = writeFileSync; + fs.readFileSync = readFileSync; +}); + +jest.mock('jest-file-exists', () => () => true); test('keyToTestName()', () => { expect(keyToTestName('abc cde 12')).toBe('abc cde'); @@ -49,7 +62,11 @@ test('saveSnapshotFile() works with \r\n', () => { saveSnapshotFile(data, filename); expect(fs.writeFileSync) - .toBeCalledWith(filename, 'exports[`myKey`] = `
\n
`;\n'); + .toBeCalledWith( + filename, + `// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` + + 'exports[`myKey`] = `
\n
`;\n' + ); }); test('saveSnapshotFile() works with \r', () => { @@ -60,7 +77,73 @@ test('saveSnapshotFile() works with \r', () => { saveSnapshotFile(data, filename); expect(fs.writeFileSync) - .toBeCalledWith(filename, 'exports[`myKey`] = `
\n
`;\n'); + .toBeCalledWith( + 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'); + fs.readFileSync = jest.fn(() => 'exports[`myKey`] = `
\n
`;\n'); + const update = false; + + expect(() => getSnapshotData(filename, update)).toThrowError( + `Outdated snapshot: No snapshot header found. ` + + `Jest 19 introduced versioned snapshots to ensure all people 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'); + fs.readFileSync = jest.fn(() => + `// Jest Snapshot v0.99, ${SNAPSHOT_GUIDE_LINK}\n\n` + + 'exports[`myKey`] = `
\n
`;\n' + ); + const update = false; + + expect(() => getSnapshotData(filename, update)).toThrowError( + `Outdated snapshot: The version of the snapshot file associated ` + + `with this test is outdated. The snapshot file version ensures that ` + + `all people 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: v0.99\n\n` + + SNAPSHOT_VERSION_WARNING + ); +}); + +test('getSnapshotData() throws for newer snapshot version', () => { + const filename = path.join(__dirname, 'old-snapshot.snap'); + fs.readFileSync = jest.fn(() => + `// Jest Snapshot v2, ${SNAPSHOT_GUIDE_LINK}\n\n` + + 'exports[`myKey`] = `
\n
`;\n' + ); + const update = false; + + expect(() => getSnapshotData(filename, update)).toThrowError( + `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 people 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: v2` + ); +}); + +test('getSnapshotData() does not throw for when updating', () => { + const filename = path.join(__dirname, 'old-snapshot.snap'); + fs.readFileSync = jest.fn(() => + 'exports[`myKey`] = `
\n
`;\n' + ); + const update = true; + + expect(() => getSnapshotData(filename, update)).not.toThrow(); }); test('escaping', () => { @@ -68,7 +151,11 @@ test('escaping', () => { const data = '"\'\\'; saveSnapshotFile({key: data}, filename); const writtenData = fs.writeFileSync.mock.calls[0][1]; - expect(writtenData).toBe("exports[`key`] = `\"'\\\\`;\n"); + expect(writtenData) + .toBe( + `// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` + + "exports[`key`] = `\"'\\\\`;\n" + ); // eslint-disable-next-line no-unused-vars const exports = {}; diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index e6f22a45cca9..09865ab6c4a2 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -21,6 +21,55 @@ const naturalCompare = require('natural-compare'); const getSerializers = require('./plugins').getSerializers; const SNAPSHOT_EXTENSION = 'snap'; +const SNAPSHOT_VERSION = '1'; +const SNAPSHOT_VERSION_REGEXP = /^\/\/ Jest Snapshot v(.+),/; +const SNAPSHOT_GUIDE_LINK = 'https://goo.gl/fbAQLP'; +const SNAPSHOT_VERSION_WARNING = + `Warning: It is advised to revert any local changes to tests or ` + + `other code during this upgrade to ensure that no invalid state ` + + `is stored as a snapshot.`; + +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) { + throw new Error( + `Outdated snapshot: No snapshot header found. ` + + `Jest 19 introduced versioned snapshots to ensure all people 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) { + throw new Error( + `Outdated snapshot: The version of the snapshot file associated ` + + `with this test is outdated. The snapshot file version ensures that ` + + `all people 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) { + throw new Error( + `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 people 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}` + ); + } +}; const testNameToKey = (testName: string, count: number) => testName + ' ' + count; @@ -38,19 +87,32 @@ const getSnapshotPath = (testPath: Path) => path.join( path.basename(testPath) + '.' + SNAPSHOT_EXTENSION, ); -const getSnapshotData = (snapshotPath: Path) => { +const getSnapshotData = (snapshotPath: Path, update: boolean) => { const data = Object.create(null); + let snapshotContents; + let dirty = false; if (fileExists(snapshotPath)) { try { - delete require.cache[require.resolve(snapshotPath)]; - /* eslint-disable no-useless-call */ - Object.assign(data, require.call(null, snapshotPath)); - /* eslint-enable no-useless-call */ + snapshotContents = fs.readFileSync(snapshotPath, 'utf8'); + // eslint-disable-next-line no-new-func + const populate = new Function('exports', snapshotContents); + populate(data); } catch (e) {} } - return data; + if (!update && snapshotContents) { + validateSnapshotVersion(snapshotContents); + } + + if (update && snapshotContents) { + try { + validateSnapshotVersion(snapshotContents); + } catch (error) { + dirty = true; + } + } + return {data, dirty}; }; // Extra line breaks at the beginning and at the end of the snapshot are useful @@ -89,15 +151,21 @@ const saveSnapshotFile = ( const snapshots = Object.keys(snapshotData).sort(naturalCompare) .map(key => 'exports[' + printBacktickString(key) + '] = ' + - printBacktickString(normalizeNewlines(snapshotData[key])) + ';', + printBacktickString(normalizeNewlines(snapshotData[key])) + ';', ); ensureDirectoryExists(snapshotPath); - fs.writeFileSync(snapshotPath, snapshots.join('\n\n') + '\n'); + fs.writeFileSync( + snapshotPath, + writeSnapshotVersion() + '\n\n' + snapshots.join('\n\n') + '\n' + ); }; module.exports = { SNAPSHOT_EXTENSION, + SNAPSHOT_GUIDE_LINK, + SNAPSHOT_VERSION, + SNAPSHOT_VERSION_WARNING, ensureDirectoryExists, getSnapshotData, getSnapshotPath, diff --git a/packages/jest-util/src/__tests__/__snapshots__/FakeTimers-test.js.snap b/packages/jest-util/src/__tests__/__snapshots__/FakeTimers-test.js.snap index b810300ba283..235d2ba741f5 100644 --- a/packages/jest-util/src/__tests__/__snapshots__/FakeTimers-test.js.snap +++ b/packages/jest-util/src/__tests__/__snapshots__/FakeTimers-test.js.snap @@ -1,3 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + exports[`FakeTimers runAllTimers warns when trying to advance timers while real timers are used 1`] = ` "A function to advance timers was called but the timers API is not mocked with fake timers. Call \`jest.useFakeTimers()\` in this test or enable fake timers globally by setting \`\\"timers\\": \\"fake\\"\` in the configuration file. This warning is likely a result of a default configuration change in Jest 15. diff --git a/packages/jest-util/src/__tests__/__snapshots__/validateCLIOptions-test.js.snap b/packages/jest-util/src/__tests__/__snapshots__/validateCLIOptions-test.js.snap index d10942de03d5..fc92881b4bc8 100644 --- a/packages/jest-util/src/__tests__/__snapshots__/validateCLIOptions-test.js.snap +++ b/packages/jest-util/src/__tests__/__snapshots__/validateCLIOptions-test.js.snap @@ -1,3 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + exports[`fails for multiple unknown options 1`] = ` "● Unrecognized CLI Parameters: diff --git a/packages/jest-validate/src/__tests__/__snapshots__/validate-test.js.snap b/packages/jest-validate/src/__tests__/__snapshots__/validate-test.js.snap index 20004537cce0..21bc4c709443 100644 --- a/packages/jest-validate/src/__tests__/__snapshots__/validate-test.js.snap +++ b/packages/jest-validate/src/__tests__/__snapshots__/validate-test.js.snap @@ -1,3 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + exports[`displays warning for deprecated config options 1`] = ` "● Deprecation Warning: diff --git a/packages/pretty-format/src/__tests__/__snapshots__/pretty-format-test.js.snap b/packages/pretty-format/src/__tests__/__snapshots__/pretty-format-test.js.snap index 668772c55b33..6a0a533778f5 100644 --- a/packages/pretty-format/src/__tests__/__snapshots__/pretty-format-test.js.snap +++ b/packages/pretty-format/src/__tests__/__snapshots__/pretty-format-test.js.snap @@ -1,3 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + exports[`prettyFormat() ReactTestComponent and ReactElement plugins ReactElement plugin highlights syntax 1`] = ` "