diff --git a/integration_tests/__tests__/__snapshots__/showConfig-test.js.snap b/integration_tests/__tests__/__snapshots__/showConfig-test.js.snap index 81c85ca11493..0c7113fdb292 100644 --- a/integration_tests/__tests__/__snapshots__/showConfig-test.js.snap +++ b/integration_tests/__tests__/__snapshots__/showConfig-test.js.snap @@ -78,6 +78,7 @@ exports[`jest --showConfig outputs config info and exits 1`] = ` "rootDir": "/mocked/root/path/jest/integration_tests/verbose_reporter", "testPathPattern": "", "testResultsProcessor": null, + "updateSnapshot": "all", "useStderr": false, "verbose": null, "watch": false, diff --git a/integration_tests/__tests__/__snapshots__/snapshot-test.js.snap b/integration_tests/__tests__/__snapshots__/snapshot-test.js.snap index bbd998778a55..90db2bdc908c 100644 --- a/integration_tests/__tests__/__snapshots__/snapshot-test.js.snap +++ b/integration_tests/__tests__/__snapshots__/snapshot-test.js.snap @@ -36,6 +36,15 @@ Ran all test suites. " `; +exports[`Snapshot Validation does not save snapshots in CI mode by default 1`] = ` +"Test Suites: 3 failed, 3 total +Tests: 7 failed, 2 passed, 9 total +Snapshots: 9 failed, 9 total +Time: <> +Ran all test suites. +" +`; + exports[`Snapshot Validation updates the snapshot when a test removes some snapshots 1`] = ` "Test Suites: 3 passed, 3 total Tests: 9 passed, 9 total @@ -72,7 +81,7 @@ Ran all test suites. " `; -exports[`Snapshot works as expected 1`] = ` +exports[`Snapshot stores new snapshots on the first run 1`] = ` "Test Suites: 2 passed, 2 total Tests: 5 passed, 5 total Snapshots: 5 added, 5 total diff --git a/integration_tests/__tests__/showConfig-test.js b/integration_tests/__tests__/showConfig-test.js index 8627e2a9f969..98ec702ca3a5 100644 --- a/integration_tests/__tests__/showConfig-test.js +++ b/integration_tests/__tests__/showConfig-test.js @@ -34,7 +34,12 @@ describe('jest --showConfig', () => { .replace(/"cacheDirectory": "(.+)"/, '"cacheDirectory": "/tmp/jest"'), test: val => typeof val === 'string', }); - const {stdout} = runJest(dir, ['--showConfig', '--no-cache']); + const {stdout} = runJest(dir, [ + '--showConfig', + '--no-cache', + // Make the snapshot flag stable on CI. + '--updateSnapshot', + ]); expect(stdout).toMatchSnapshot(); }); }); diff --git a/integration_tests/__tests__/snapshot-serializers-test.js b/integration_tests/__tests__/snapshot-serializers-test.js index eea69cd2e89d..de6a03a2559e 100644 --- a/integration_tests/__tests__/snapshot-serializers-test.js +++ b/integration_tests/__tests__/snapshot-serializers-test.js @@ -17,7 +17,11 @@ const snapshotsDir = path.resolve(testDir, '__tests__/__snapshots__'); const snapshotPath = path.resolve(snapshotsDir, 'snapshot-test.js.snap'); const runAndAssert = () => { - const result = runJest.json('snapshot-serializers', ['--no-cache']); + const result = runJest.json('snapshot-serializers', [ + '-w=1', + '--ci=false', + '--no-cache', + ]); const json = result.json; expect(json.numTotalTests).toBe(7); expect(json.numPassedTests).toBe(7); diff --git a/integration_tests/__tests__/snapshot-test.js b/integration_tests/__tests__/snapshot-test.js index a34b5aa67981..420497cdf7ce 100644 --- a/integration_tests/__tests__/snapshot-test.js +++ b/integration_tests/__tests__/snapshot-test.js @@ -100,8 +100,8 @@ describe('Snapshot', () => { beforeEach(cleanup); afterAll(cleanup); - it('works as expected', () => { - const result = runJest.json('snapshot', []); + it('stores new snapshots on the first run', () => { + const result = runJest.json('snapshot', ['-w=1', '--ci=false']); const json = result.json; expect(json.numTotalTests).toBe(5); @@ -122,7 +122,11 @@ describe('Snapshot', () => { it('works with escaped characters', () => { // Write the first snapshot - let result = runJest('snapshot-escape', ['snapshot-test.js']); + let result = runJest('snapshot-escape', [ + '-w=1', + '--ci=false', + 'snapshot-test.js', + ]); let stderr = result.stderr.toString(); expect(stderr).toMatch('1 snapshot written'); @@ -136,7 +140,12 @@ describe('Snapshot', () => { const newTestData = initialTestData + testData; fs.writeFileSync(snapshotEscapeTestFile, newTestData, 'utf8'); - result = runJest('snapshot-escape', ['snapshot-test.js']); + result = runJest('snapshot-escape', [ + '-w=1', + '--ci=false', + '--updateSnapshot', + 'snapshot-test.js', + ]); stderr = result.stderr.toString(); expect(stderr).toMatch('1 snapshot written'); @@ -145,7 +154,11 @@ describe('Snapshot', () => { // Now let's check again if everything still passes. // If this test doesn't pass, some snapshot data was not properly escaped. - result = runJest('snapshot-escape', ['snapshot-test.js']); + result = runJest('snapshot-escape', [ + '-w=1', + '--ci=false', + 'snapshot-test.js', + ]); stderr = result.stderr.toString(); expect(stderr).not.toMatch('Snapshot Summary'); @@ -155,14 +168,22 @@ describe('Snapshot', () => { it('works with escaped regex', () => { // Write the first snapshot - let result = runJest('snapshot-escape', ['snapshot-escape-regex.js']); + let result = runJest('snapshot-escape', [ + '-w=1', + '--ci=false', + 'snapshot-escape-regex.js', + ]); let stderr = result.stderr.toString(); expect(stderr).toMatch('2 snapshots written in 1 test suite.'); expect(result.status).toBe(0); expect(extractSummary(stderr).summary).toMatchSnapshot(); - result = runJest('snapshot-escape', ['snapshot-escape-regex.js']); + result = runJest('snapshot-escape', [ + '-w=1', + '--ci=false', + 'snapshot-escape-regex.js', + ]); stderr = result.stderr.toString(); // Make sure we aren't writing a snapshot this time which would @@ -175,6 +196,8 @@ describe('Snapshot', () => { it('works with template literal subsitutions', () => { // Write the first snapshot let result = runJest('snapshot-escape', [ + '-w=1', + '--ci=false', 'snapshot-escape-substitution-test.js', ]); let stderr = result.stderr.toString(); @@ -184,6 +207,8 @@ describe('Snapshot', () => { expect(extractSummary(stderr).summary).toMatchSnapshot(); result = runJest('snapshot-escape', [ + '-w=1', + '--ci=false', 'snapshot-escape-substitution-test.js', ]); stderr = result.stderr.toString(); @@ -200,8 +225,21 @@ describe('Snapshot', () => { fs.writeFileSync(copyOfTestPath, originalTestContent); }); + it('does not save snapshots in CI mode by default', () => { + const result = runJest.json('snapshot', ['-w=1', '--ci=true']); + + expect(result.json.success).toBe(false); + expect(result.json.numTotalTests).toBe(9); + expect(result.json.snapshot.added).toBe(0); + expect(result.json.snapshot.total).toBe(9); + const {rest, summary} = extractSummary(result.stderr.toString()); + + expect(rest).toMatch('New snapshot was not written'); + expect(summary).toMatchSnapshot(); + }); + it('works on subsequent runs without `-u`', () => { - const firstRun = runJest.json('snapshot', []); + const firstRun = runJest.json('snapshot', ['-w=1', '--ci=false']); const content = require(snapshotOfCopy); expect(content).not.toBe(undefined); @@ -220,12 +258,12 @@ describe('Snapshot', () => { }); it('deletes the snapshot if the test suite has been removed', () => { - const firstRun = runJest.json('snapshot', []); + const firstRun = runJest.json('snapshot', ['-w=1', '--ci=false']); fs.unlinkSync(copyOfTestPath); const content = require(snapshotOfCopy); expect(content).not.toBe(undefined); - const secondRun = runJest.json('snapshot', ['-u']); + const secondRun = runJest.json('snapshot', ['-w=1', '--ci=false', '-u']); expect(firstRun.json.numTotalTests).toBe(9); expect(secondRun.json.numTotalTests).toBe(5); @@ -240,10 +278,10 @@ describe('Snapshot', () => { }); it('deletes a snapshot when a test does removes all the snapshots', () => { - const firstRun = runJest.json('snapshot', []); + const firstRun = runJest.json('snapshot', ['-w=1', '--ci=false']); fs.writeFileSync(copyOfTestPath, emptyTest); - const secondRun = runJest.json('snapshot', ['-u']); + const secondRun = runJest.json('snapshot', ['-w=1', '--ci=false', '-u']); fs.unlinkSync(copyOfTestPath); expect(firstRun.json.numTotalTests).toBe(9); @@ -259,7 +297,7 @@ describe('Snapshot', () => { }); it('updates the snapshot when a test removes some snapshots', () => { - const firstRun = runJest.json('snapshot', []); + const firstRun = runJest.json('snapshot', ['-w=1', '--ci=false']); fs.unlinkSync(copyOfTestPath); const beforeRemovingSnapshot = getSnapshotOfCopy(); @@ -270,7 +308,7 @@ describe('Snapshot', () => { '.not.toBe(undefined)', ), ); - const secondRun = runJest.json('snapshot', ['-u']); + const secondRun = runJest.json('snapshot', ['-w=1', '--ci=false', '-u']); fs.unlinkSync(copyOfTestPath); expect(firstRun.json.numTotalTests).toBe(9); diff --git a/integration_tests/__tests__/toMatchSnapshot-test.js b/integration_tests/__tests__/toMatchSnapshot-test.js index 459de5611fc9..4750f2aec2d2 100644 --- a/integration_tests/__tests__/toMatchSnapshot-test.js +++ b/integration_tests/__tests__/toMatchSnapshot-test.js @@ -26,13 +26,13 @@ test('basic support', () => { { makeTests(TESTS_DIR, {[filename]: template(['{apple: "original value"}'])}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('1 snapshot written in 1 test suite.'); expect(status).toBe(0); } { - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('Snapshots: 1 passed, 1 total'); expect(stderr).not.toMatch('1 snapshot written in 1 test suite.'); expect(status).toBe(0); @@ -40,13 +40,18 @@ test('basic support', () => { { makeTests(TESTS_DIR, {[filename]: template(['{apple: "updated value"}'])}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('Received value does not match stored snapshot'); expect(status).toBe(1); } { - const {stderr, status} = runJest(DIR, [filename, '-u']); + const {stderr, status} = runJest(DIR, [ + '-w=1', + '--ci=false', + filename, + '-u', + ]); expect(stderr).toMatch('1 snapshot updated in 1 test suite.'); expect(status).toBe(0); } @@ -54,29 +59,27 @@ test('basic support', () => { test('error thrown before snapshot', () => { const filename = 'error-thrown-before-snapshot-test.js'; - const template = makeTemplate( - `test('snapshots', () => { + const template = makeTemplate(`test('snapshots', () => { expect($1).toBeTruthy(); expect($2).toMatchSnapshot(); - });`, - ); + });`); { makeTests(TESTS_DIR, {[filename]: template(['true', '{a: "original"}'])}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('1 snapshot written in 1 test suite.'); expect(status).toBe(0); } { - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('Snapshots: 1 passed, 1 total'); expect(status).toBe(0); } { makeTests(TESTS_DIR, {[filename]: template(['false', '{a: "original"}'])}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).not.toMatch('1 obsolete snapshot found'); expect(status).toBe(1); } @@ -84,23 +87,21 @@ test('error thrown before snapshot', () => { test('first snapshot fails, second passes', () => { const filename = 'first-snapshot-fails-second-passes-test.js'; - const template = makeTemplate( - `test('snapshots', () => { + const template = makeTemplate(`test('snapshots', () => { expect($1).toMatchSnapshot(); expect($2).toMatchSnapshot(); - });`, - ); + });`); { makeTests(TESTS_DIR, {[filename]: template([`'apple'`, `'banana'`])}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('2 snapshots written in 1 test suite.'); expect(status).toBe(0); } { makeTests(TESTS_DIR, {[filename]: template([`'kiwi'`, `'banana'`])}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('Received value does not match stored snapshot'); expect(stderr).toMatch('- "apple"\n + "kiwi"'); expect(stderr).not.toMatch('1 obsolete snapshot found'); @@ -110,27 +111,25 @@ test('first snapshot fails, second passes', () => { test('does not mark snapshots as obsolete in skipped tests', () => { const filename = 'no-obsolete-if-skipped-test.js'; - const template = makeTemplate( - `test('snapshots', () => { + const template = makeTemplate(`test('snapshots', () => { expect(true).toBe(true); }); $1('will be skipped', () => { expect({a: 6}).toMatchSnapshot(); }); - `, - ); + `); { makeTests(TESTS_DIR, {[filename]: template(['test'])}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('1 snapshot written in 1 test suite.'); expect(status).toBe(0); } { makeTests(TESTS_DIR, {[filename]: template(['test.skip'])}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).not.toMatch('1 obsolete snapshot found'); expect(status).toBe(0); } @@ -138,16 +137,14 @@ test('does not mark snapshots as obsolete in skipped tests', () => { test('accepts custom snapshot name', () => { const filename = 'accept-custom-snapshot-name-test.js'; - const template = makeTemplate( - `test('accepts custom snapshot name', () => { + const template = makeTemplate(`test('accepts custom snapshot name', () => { expect(true).toMatchSnapshot('custom-name'); }); - `, - ); + `); { makeTests(TESTS_DIR, {[filename]: template()}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('1 snapshot written in 1 test suite.'); expect(status).toBe(0); } diff --git a/integration_tests/__tests__/toThrowErrorMatchingSnapshot-test.js b/integration_tests/__tests__/toThrowErrorMatchingSnapshot-test.js index a5f5ce3b0b2e..93867b68b1b5 100644 --- a/integration_tests/__tests__/toThrowErrorMatchingSnapshot-test.js +++ b/integration_tests/__tests__/toThrowErrorMatchingSnapshot-test.js @@ -20,17 +20,15 @@ afterAll(() => cleanup(TESTS_DIR)); test('works fine when function throws error', () => { const filename = 'works-fine-when-function-throws-error-test.js'; - const template = makeTemplate( - `test('works fine when function throws error', () => { + const template = makeTemplate(`test('works fine when function throws error', () => { expect(() => { throw new Error('apple'); }) .toThrowErrorMatchingSnapshot(); }); - `, - ); + `); { makeTests(TESTS_DIR, {[filename]: template()}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('1 snapshot written in 1 test suite.'); expect(status).toBe(0); } @@ -38,16 +36,14 @@ test('works fine when function throws error', () => { test(`throws the error if tested function didn't throw error`, () => { const filename = 'throws-if-tested-function-did-not-throw-test.js'; - const template = makeTemplate( - `test('throws the error if tested function did not throw error', () => { + const template = makeTemplate(`test('throws the error if tested function did not throw error', () => { expect(() => {}).toThrowErrorMatchingSnapshot(); }); - `, - ); + `); { makeTests(TESTS_DIR, {[filename]: template()}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch(`Expected the function to throw an error.`); expect(status).toBe(1); } @@ -55,17 +51,15 @@ test(`throws the error if tested function didn't throw error`, () => { test('does not accept arguments', () => { const filename = 'does-not-accept-arguments-test.js'; - const template = makeTemplate( - `test('does not accept arguments', () => { + const template = makeTemplate(`test('does not accept arguments', () => { expect(() => { throw new Error('apple'); }) .toThrowErrorMatchingSnapshot('foobar'); }); - `, - ); + `); { makeTests(TESTS_DIR, {[filename]: template()}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('Matcher does not accept any arguments.'); expect(status).toBe(1); } @@ -73,16 +67,14 @@ test('does not accept arguments', () => { test('cannot be used with .not', () => { const filename = 'cannot-be-used-with-not-test.js'; - const template = makeTemplate( - `test('cannot be used with .not', () => { + const template = makeTemplate(`test('cannot be used with .not', () => { expect('').not.toThrowErrorMatchingSnapshot(); }); - `, - ); + `); { makeTests(TESTS_DIR, {[filename]: template()}); - const {stderr, status} = runJest(DIR, [filename]); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch( 'Jest: `.not` cannot be used with `.toThrowErrorMatchingSnapshot()`.', ); diff --git a/packages/jest-cli/src/TestRunner.js b/packages/jest-cli/src/TestRunner.js index 02eab0d5ada6..1452f3f4a42f 100644 --- a/packages/jest-cli/src/TestRunner.js +++ b/packages/jest-cli/src/TestRunner.js @@ -140,9 +140,9 @@ class TestRunner { ); aggregatedResults.snapshot.filesRemoved += status.filesRemoved; }); - aggregatedResults.snapshot.didUpdate = this._globalConfig.updateSnapshot; - aggregatedResults.snapshot.failure = !!(!this._globalConfig - .updateSnapshot && + const updateAll = this._globalConfig.updateSnapshot === 'all'; + aggregatedResults.snapshot.didUpdate = updateAll; + aggregatedResults.snapshot.failure = !!(!updateAll && (aggregatedResults.snapshot.unchecked || aggregatedResults.snapshot.unmatched || aggregatedResults.snapshot.filesRemoved)); diff --git a/packages/jest-cli/src/__tests__/watch-test.js b/packages/jest-cli/src/__tests__/watch-test.js index 8b2b05781d55..7842fb750384 100644 --- a/packages/jest-cli/src/__tests__/watch-test.js +++ b/packages/jest-cli/src/__tests__/watch-test.js @@ -142,7 +142,7 @@ describe('Watch mode flows', () => { stdin.emit(KEYS.U); expect(runJestMock.mock.calls[0][0]).toEqual({ - updateSnapshot: true, + updateSnapshot: 'all', watch: true, }); diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index 4a4b4e206f55..5f166d3eff43 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -12,6 +12,8 @@ import type {Argv} from 'types/Argv'; +const isCI = require('is-ci'); + const check = (argv: Argv) => { if (argv.runInBand && argv.hasOwnProperty('maxWorkers')) { throw new Error( @@ -80,6 +82,13 @@ const options = { ' dependency information.', type: 'string', }, + ci: { + default: isCI, + description: 'Whether to run Jest in continuous integration (CI) mode. ' + + 'This option is on by default in most popular CI environments. It will ' + + ' prevent snapshots from being written unless explicitly requested.', + type: 'boolean', + }, clearMocks: { default: undefined, description: 'Automatically clear mock calls and instances between every ' + diff --git a/packages/jest-cli/src/watch.js b/packages/jest-cli/src/watch.js index 35937b273eb1..ff25e409775f 100644 --- a/packages/jest-cli/src/watch.js +++ b/packages/jest-cli/src/watch.js @@ -174,7 +174,7 @@ const watch = ( startRun(); break; case KEYS.U: - startRun({updateSnapshot: true}); + startRun({updateSnapshot: 'all'}); break; case KEYS.A: updateArgv(argv, 'watchAll', { diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index 72f95cdc0472..9b1c0a25e01c 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -426,7 +426,6 @@ function normalize(options: InitialOptions, argv: Argv) { case 'testRegex': case 'testURL': case 'timers': - case 'updateSnapshot': case 'useStderr': case 'verbose': case 'watch': @@ -438,6 +437,10 @@ function normalize(options: InitialOptions, argv: Argv) { return newOptions; }, newOptions); + newOptions.updateSnapshot = argv.ci && !argv.updateSnapshot + ? 'none' + : argv.updateSnapshot ? 'all' : 'new'; + if (babelJest) { const regeneratorRuntimePath = Resolver.findNodeModule( 'regenerator-runtime/runtime', diff --git a/packages/jest-editor-support/src/__tests__/TestReconciler-test.js b/packages/jest-editor-support/src/__tests__/TestReconciler-test.js index 8ec02216d382..4a336913d2d4 100644 --- a/packages/jest-editor-support/src/__tests__/TestReconciler-test.js +++ b/packages/jest-editor-support/src/__tests__/TestReconciler-test.js @@ -47,12 +47,10 @@ describe('Test Reconciler', () => { expect(status.line).toEqual(12); const errorMessage = 'Expected value to be falsy, instead received true'; expect(status.terseMessage).toEqual(errorMessage); - expect(status.shortMessage).toEqual( - `Error: expect(received).toBeFalsy() + expect(status.shortMessage).toEqual(`Error: expect(received).toBeFalsy() Expected value to be falsy, instead received - true`, - ); + true`); }); }); }); diff --git a/packages/jest-jasmine2/src/index.js b/packages/jest-jasmine2/src/index.js index b23589f4ec8f..2077b09cda3d 100644 --- a/packages/jest-jasmine2/src/index.js +++ b/packages/jest-jasmine2/src/index.js @@ -11,6 +11,7 @@ import type {Environment} from 'types/Environment'; import type {GlobalConfig, ProjectConfig} from 'types/Config'; +import type {SnapshotState} from 'jest-snapshot'; import type {TestResult} from 'types/TestResult'; import type Runtime from 'jest-runtime'; @@ -75,7 +76,7 @@ function jasmine2( expand: globalConfig.expand, }); - const snapshotState = runtime.requireInternalModule( + const snapshotState: SnapshotState = runtime.requireInternalModule( path.resolve(__dirname, './setup-jest-globals.js'), )({ config, @@ -97,12 +98,10 @@ function jasmine2( env.execute(); return reporter .getResults() - .then(results => - addSnapshotData(results, snapshotState, globalConfig.updateSnapshot), - ); + .then(results => addSnapshotData(results, snapshotState)); } -const addSnapshotData = (results, snapshotState, updateSnapshot) => { +const addSnapshotData = (results, snapshotState) => { results.testResults.forEach(({fullName, status}) => { if (status === 'pending' || status === 'failed') { // if test is skipped or failed, we don't want to mark @@ -112,11 +111,11 @@ const addSnapshotData = (results, snapshotState, updateSnapshot) => { }); const uncheckedCount = snapshotState.getUncheckedCount(); - if (updateSnapshot) { + if (uncheckedCount) { snapshotState.removeUncheckedKeys(); } - const status = snapshotState.save(updateSnapshot); + const status = snapshotState.save(); results.snapshot.fileDeleted = status.deleted; results.snapshot.added = snapshotState.added; results.snapshot.matched = snapshotState.matched; diff --git a/packages/jest-jasmine2/src/setup-jest-globals.js b/packages/jest-jasmine2/src/setup-jest-globals.js index 57bbbc0b643e..15bf2a500481 100644 --- a/packages/jest-jasmine2/src/setup-jest-globals.js +++ b/packages/jest-jasmine2/src/setup-jest-globals.js @@ -14,7 +14,7 @@ import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; import type {Plugin} from 'types/PrettyFormat'; const {getState, setState} = require('jest-matchers'); -const {initializeSnapshotState, addSerializer} = require('jest-snapshot'); +const {SnapshotState, addSerializer} = require('jest-snapshot'); const { EXPECTED_COLOR, RECEIVED_COLOR, @@ -142,15 +142,10 @@ module.exports = ({ config.snapshotSerializers.concat().reverse().forEach(path => { addSerializer(localRequire(path)); }); - setState({testPath}); patchJasmine(); - const snapshotState = initializeSnapshotState( - testPath, - globalConfig.updateSnapshot, - '', - globalConfig.expand, - ); - setState({snapshotState}); + const {expand, updateSnapshot} = globalConfig; + const snapshotState = new SnapshotState(testPath, {expand, updateSnapshot}); + setState({snapshotState, testPath}); // Return it back to the outer scope (test runner outside the VM). return snapshotState; }; diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index 8243287c241c..ec5d7078a8e0 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -10,7 +10,7 @@ 'use strict'; -import type {Path} from 'types/Config'; +import type {Path, SnapshotUpdateState} from 'types/Config'; const { saveSnapshotFile, @@ -23,43 +23,43 @@ const { } = require('./utils'); const fs = require('fs'); +export type SnapshotStateOptions = {| + updateSnapshot: SnapshotUpdateState, + snapshotPath?: string, + expand?: boolean, +|}; + class SnapshotState { _counters: Map; _dirty: boolean; _index: number; + _updateSnapshot: SnapshotUpdateState; _snapshotData: {[key: string]: string}; _snapshotPath: Path; _uncheckedKeys: Set; added: number; expand: boolean; - failedTests: Set; matched: number; - skippedTests: Set; unmatched: number; - update: boolean; updated: number; - constructor( - testPath: Path, - update: boolean, - snapshotPath?: string, - expand?: boolean, - ) { - this._snapshotPath = snapshotPath || getSnapshotPath(testPath); - const {data, dirty} = getSnapshotData(this._snapshotPath, update); + constructor(testPath: Path, options: SnapshotStateOptions) { + this._snapshotPath = options.snapshotPath || getSnapshotPath(testPath); + const {data, dirty} = getSnapshotData( + this._snapshotPath, + options.updateSnapshot, + ); this._snapshotData = data; this._dirty = dirty; this._uncheckedKeys = new Set(Object.keys(this._snapshotData)); this._counters = new Map(); this._index = 0; - this.expand = expand || false; + this.expand = options.expand || false; this.added = 0; this.matched = 0; this.unmatched = 0; - this.update = update; + this._updateSnapshot = options.updateSnapshot; this.updated = 0; - this.skippedTests = new Set(); - this.failedTests = new Set(); } markSnapshotsAsCheckedForTest(testName: string) { @@ -75,19 +75,18 @@ class SnapshotState { this._snapshotData[key] = receivedSerialized; } - save(update: boolean) { + save() { + const isEmpty = Object.keys(this._snapshotData).length === 0; const status = { deleted: false, saved: false, }; - const isEmpty = Object.keys(this._snapshotData).length === 0; - if ((this._dirty || this._uncheckedKeys.size) && !isEmpty) { saveSnapshotFile(this._snapshotData, this._snapshotPath); status.saved = true; } else if (isEmpty && fs.existsSync(this._snapshotPath)) { - if (update) { + if (this._updateSnapshot === 'all') { fs.unlinkSync(this._snapshotPath); } status.deleted = true; @@ -101,7 +100,7 @@ class SnapshotState { } removeUncheckedKeys(): void { - if (this._uncheckedKeys.size) { + if (this._updateSnapshot === 'all' && this._uncheckedKeys.size) { this._dirty = true; this._uncheckedKeys.forEach(key => delete this._snapshotData[key]); this._uncheckedKeys.clear(); @@ -133,12 +132,19 @@ class SnapshotState { this._snapshotData[key] = receivedSerialized; } + // These are the conditions on when to write snapshots: + // * There's no snapshot file in a non-CI environment. + // * There is a snapshot file and we decided to update the snapshot. + // * There is a snapshot file, but it doesn't have this snaphsot. + // These are the conditions on when not to write snapshots: + // * The update flag is set to 'none'. + // * There's no snapshot file or a file without this snapshot on a CI environment. if ( - !fs.existsSync(this._snapshotPath) || // there's no snapshot file - (hasSnapshot && this.update) || // there is a file, but we're updating - !hasSnapshot // there is a file, but it doesn't have this snaphsot + (hasSnapshot && this._updateSnapshot === 'all') || + ((!hasSnapshot || !fs.existsSync(this._snapshotPath)) && + (this._updateSnapshot === 'new' || this._updateSnapshot === 'all')) ) { - if (this.update) { + if (this._updateSnapshot === 'all') { if (!pass) { if (hasSnapshot) { this.updated++; @@ -154,19 +160,29 @@ class SnapshotState { this.added++; } - return {pass: true}; + return { + actual: '', + count, + expected: '', + pass: true, + }; } else { if (!pass) { this.unmatched++; return { actual: unescape(receivedSerialized), count, - expected: unescape(expected), + expected: expected ? unescape(expected) : null, pass: false, }; } else { this.matched++; - return {pass: true}; + return { + actual: '', + count, + expected: '', + pass: true, + }; } } } diff --git a/packages/jest-snapshot/src/__tests__/utils-test.js b/packages/jest-snapshot/src/__tests__/utils-test.js index b31243875fca..8cc976d3d2fd 100644 --- a/packages/jest-snapshot/src/__tests__/utils-test.js +++ b/packages/jest-snapshot/src/__tests__/utils-test.js @@ -87,7 +87,7 @@ test('saveSnapshotFile() works with \r', () => { 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; + const update = 'none'; expect(() => getSnapshotData(filename, update)).toThrowError( chalk.red( @@ -106,7 +106,7 @@ test('getSnapshotData() throws for older snapshot version', () => { `// Jest Snapshot v0.99, ${SNAPSHOT_GUIDE_LINK}\n\n` + 'exports[`myKey`] = `
\n
`;\n', ); - const update = false; + const update = 'none'; expect(() => getSnapshotData(filename, update)).toThrowError( chalk.red( @@ -129,7 +129,7 @@ test('getSnapshotData() throws for newer snapshot version', () => { `// Jest Snapshot v2, ${SNAPSHOT_GUIDE_LINK}\n\n` + 'exports[`myKey`] = `
\n
`;\n', ); - const update = false; + const update = 'none'; expect(() => getSnapshotData(filename, update)).toThrowError( chalk.red( @@ -148,7 +148,7 @@ test('getSnapshotData() throws for newer snapshot version', () => { 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; + const update = 'all'; expect(() => getSnapshotData(filename, update)).not.toThrow(); }); @@ -156,7 +156,7 @@ test('getSnapshotData() does not throw for when updating', () => { test('getSnapshotData() marks invalid snapshot dirty when updating', () => { const filename = path.join(__dirname, 'old-snapshot.snap'); fs.readFileSync = jest.fn(() => 'exports[`myKey`] = `
\n
`;\n'); - const update = true; + const update = 'all'; expect(getSnapshotData(filename, update)).toMatchObject({dirty: true}); }); @@ -168,7 +168,7 @@ test('getSnapshotData() marks valid snapshot not dirty when updating', () => { `// Jest Snapshot v${SNAPSHOT_VERSION}, ${SNAPSHOT_GUIDE_LINK}\n\n` + 'exports[`myKey`] = `
\n
`;\n', ); - const update = true; + const update = 'all'; expect(getSnapshotData(filename, update)).toMatchObject({dirty: false}); }); diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index 7dc1990baafa..0793a647e57d 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -10,7 +10,8 @@ 'use strict'; import type {HasteFS} from 'types/HasteMap'; -import type {Path} from 'types/Config'; +import type {MatcherState} from 'types/Matchers'; +import type {Path, SnapshotUpdateState} from 'types/Config'; const diff = require('jest-diff'); const fs = require('fs'); @@ -29,7 +30,7 @@ const {SNAPSHOT_EXTENSION} = require('./utils'); const fileExists = (filePath: Path, hasteFS: HasteFS): boolean => hasteFS.exists(filePath) || fs.existsSync(filePath); -const cleanup = (hasteFS: HasteFS, update: boolean) => { +const cleanup = (hasteFS: HasteFS, update: SnapshotUpdateState) => { const pattern = '\\.' + SNAPSHOT_EXTENSION + '$'; const files = hasteFS.matchFiles(pattern); const filesRemoved = files @@ -45,7 +46,7 @@ const cleanup = (hasteFS: HasteFS, update: boolean) => { ), ) .map(snapshotFile => { - if (update) { + if (update === 'all') { fs.unlinkSync(snapshotFile); } }).length; @@ -55,17 +56,10 @@ const cleanup = (hasteFS: HasteFS, update: boolean) => { }; }; -const initializeSnapshotState = ( - testFile: Path, - update: boolean, - testPath: string, - expand: boolean, -) => new SnapshotState(testFile, update, testPath, expand); - const toMatchSnapshot = function(received: any, testName?: string) { this.dontThrow && this.dontThrow(); - const {currentTestName, isNot, snapshotState} = this; + const {currentTestName, isNot, snapshotState}: MatcherState = this; if (isNot) { throw new Error('Jest: `.not` cannot be used with `.toMatchSnapshot()`.'); @@ -75,45 +69,51 @@ const toMatchSnapshot = function(received: any, testName?: string) { throw new Error('Jest: snapshot state must be initialized.'); } - const result = snapshotState.match(testName || currentTestName, received); - const {pass} = result; + const result = snapshotState.match( + testName || currentTestName || '', + received, + ); + const {count, pass} = result; + let {actual, expected} = result; + let report; if (pass) { return {message: '', pass: true}; + } else if (!expected) { + report = () => + `New snapshot was ${RECEIVED_COLOR('not written')}. The update flag ` + + `must be explicitly passed to write a new snapshot.\n\n` + + `This is likely because this test is run in a continuous integration ` + + `(CI) environment in which snapshots are not written by default.`; } else { - const {count, expected, actual} = result; - - const expectedString = expected.trim(); - const actualString = actual.trim(); - const diffMessage = diff(expectedString, actualString, { + expected = (expected || '').trim(); + actual = (actual || '').trim(); + const diffMessage = diff(expected, actual, { aAnnotation: 'Snapshot', bAnnotation: 'Received', expand: snapshotState.expand, }); - const report = () => + report = () => `${RECEIVED_COLOR('Received value')} does not match ` + `${EXPECTED_COLOR('stored snapshot ' + count)}.\n\n` + (diffMessage || - RECEIVED_COLOR('- ' + expectedString) + + RECEIVED_COLOR('- ' + (expected || '')) + '\n' + - EXPECTED_COLOR('+ ' + actualString)); - - const message = () => - matcherHint('.toMatchSnapshot', 'value', '') + '\n\n' + report(); - - // Passing the the actual and expected objects so that a custom reporter - // could access them, for example in order to display a custom visual diff, - // or create a different error message - return { - actual: actualString, - expected: expectedString, - message, - name: 'toMatchSnapshot', - pass: false, - report, - }; + EXPECTED_COLOR('+ ' + actual)); } + // Passing the the actual and expected objects so that a custom reporter + // could access them, for example in order to display a custom visual diff, + // or create a different error message + return { + actual, + expected, + message: () => + matcherHint('.toMatchSnapshot', 'value', '') + '\n\n' + report(), + name: 'toMatchSnapshot', + pass: false, + report, + }; }; const toThrowErrorMatchingSnapshot = function(received: any, expected: void) { @@ -154,7 +154,6 @@ module.exports = { addSerializer, cleanup, getSerializers, - initializeSnapshotState, toMatchSnapshot, toThrowErrorMatchingSnapshot, }; diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index 62166cd313fb..20efa637d598 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -10,7 +10,7 @@ 'use strict'; -import type {Path} from 'types/Config'; +import type {Path, SnapshotUpdateState} from 'types/Config'; const chalk = require('chalk'); const createDirectory = require('jest-util').createDirectory; @@ -97,7 +97,7 @@ const getSnapshotPath = (testPath: Path) => path.basename(testPath) + '.' + SNAPSHOT_EXTENSION, ); -const getSnapshotData = (snapshotPath: Path, update: boolean) => { +const getSnapshotData = (snapshotPath: Path, update: SnapshotUpdateState) => { const data = Object.create(null); let snapshotContents = ''; let dirty = false; @@ -114,11 +114,11 @@ const getSnapshotData = (snapshotPath: Path, update: boolean) => { const validationResult = validateSnapshotVersion(snapshotContents); const isInvalid = snapshotContents && validationResult; - if (!update && isInvalid) { + if (update === 'none' && isInvalid) { throw validationResult; } - if (update && isInvalid) { + if ((update === 'all' || update === 'new') && isInvalid) { dirty = true; } diff --git a/packages/pretty-format/src/__tests__/AsymmetricMatcher-test.js b/packages/pretty-format/src/__tests__/AsymmetricMatcher-test.js index 3c6525ebe00b..c07c73b0933d 100644 --- a/packages/pretty-format/src/__tests__/AsymmetricMatcher-test.js +++ b/packages/pretty-format/src/__tests__/AsymmetricMatcher-test.js @@ -65,21 +65,17 @@ test(`anything()`, () => { test(`arrayContaining()`, () => { const result = prettyFormat(expect.arrayContaining([1, 2]), options); - expect(result).toEqual( - `ArrayContaining [ + expect(result).toEqual(`ArrayContaining [ 1, 2, -]`, - ); +]`); }); test(`objectContaining()`, () => { const result = prettyFormat(expect.objectContaining({a: 'test'}), options); - expect(result).toEqual( - `ObjectContaining { + expect(result).toEqual(`ObjectContaining { "a": "test", -}`, - ); +}`); }); test(`stringContaining(string)`, () => { @@ -113,8 +109,7 @@ test(`supports multiple nested asymmetric matchers`, () => { }, options, ); - expect(result).toEqual( - `Object { + expect(result).toEqual(`Object { "test": Object { "nested": ObjectContaining { "a": ArrayContaining [ @@ -129,8 +124,7 @@ test(`supports multiple nested asymmetric matchers`, () => { }, }, }, -}`, - ); +}`); }); test(`supports minified output`, () => { diff --git a/types/Argv.js b/types/Argv.js index ee9c8bc2c0f4..d98a99f3e80f 100644 --- a/types/Argv.js +++ b/types/Argv.js @@ -18,6 +18,7 @@ export type Argv = {| cache: boolean, cacheDirectory: string, clearMocks: boolean, + ci: boolean, collectCoverage: boolean, collectCoverageFrom: Array, collectCoverageOnlyFrom: Array, diff --git a/types/Config.js b/types/Config.js index 0526b5d3fca1..71abd41e7341 100644 --- a/types/Config.js +++ b/types/Config.js @@ -123,6 +123,8 @@ export type InitialOptions = {| watchman?: boolean, |}; +export type SnapshotUpdateState = 'all' | 'new' | 'none'; + export type GlobalConfig = {| bail: boolean, collectCoverage: boolean, @@ -145,7 +147,7 @@ export type GlobalConfig = {| testNamePattern: string, testPathPattern: string, testResultsProcessor: ?string, - updateSnapshot: boolean, + updateSnapshot: SnapshotUpdateState, useStderr: boolean, verbose: ?boolean, watch: boolean, diff --git a/types/Matchers.js b/types/Matchers.js index 30f8e18b39f7..756324fec785 100644 --- a/types/Matchers.js +++ b/types/Matchers.js @@ -10,6 +10,7 @@ 'use strict'; import type {Path} from 'types/Config'; +import type {SnapshotState} from 'jest-snapshot'; export type ExpectationResult = { pass: boolean, @@ -27,10 +28,12 @@ export type PromiseMatcherFn = (actual: any) => Promise; export type MatcherContext = {isNot: boolean}; export type MatcherState = { assertionCalls: number, - isExpectingAssertions: ?boolean, - expectedAssertionsNumber: ?number, currentTestName?: string, expand?: boolean, + expectedAssertionsNumber: ?number, + isExpectingAssertions: ?boolean, + isNot: boolean, + snapshotState: SnapshotState, suppressedErrors: Array, testPath?: Path, };