Skip to content

Commit

Permalink
Do not write snapshots by default on CI systems.
Browse files Browse the repository at this point in the history
  • Loading branch information
cpojer committed May 3, 2017
1 parent e795008 commit be3cd57
Show file tree
Hide file tree
Showing 12 changed files with 85 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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": "new",
"useStderr": false,
"verbose": null,
"watch": false,
Expand Down
3 changes: 2 additions & 1 deletion packages/jest-cli/src/TestRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ class TestRunner {
);
aggregatedResults.snapshot.filesRemoved += status.filesRemoved;
});
aggregatedResults.snapshot.didUpdate = this._globalConfig.updateSnapshot;
aggregatedResults.snapshot.didUpdate =
this._globalConfig.updateSnapshot === 'all';
aggregatedResults.snapshot.failure = !!(!this._globalConfig
.updateSnapshot &&
(aggregatedResults.snapshot.unchecked ||
Expand Down
9 changes: 9 additions & 0 deletions packages/jest-cli/src/cli/args.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 ' +
Expand Down
5 changes: 4 additions & 1 deletion packages/jest-config/src/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,6 @@ function normalize(options: InitialOptions, argv: Argv) {
case 'testRegex':
case 'testURL':
case 'timers':
case 'updateSnapshot':
case 'useStderr':
case 'verbose':
case 'watch':
Expand All @@ -432,6 +431,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',
Expand Down
9 changes: 3 additions & 6 deletions packages/jest-jasmine2/src/setup-jest-globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,10 @@ module.exports = ({
config.snapshotSerializers.concat().reverse().forEach(path => {
addSerializer(localRequire(path));
});
setState({testPath});
patchJasmine();
const snapshotState = new SnapshotState(testPath, {
expand: globalConfig.expand,
shouldUpdate: globalConfig.updateSnapshot,
});
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;
};
29 changes: 13 additions & 16 deletions packages/jest-snapshot/src/State.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

'use strict';

import type {Path} from 'types/Config';
import type {Path, SnapshotUpdateState} from 'types/Config';

const {
saveSnapshotFile,
Expand All @@ -24,7 +24,7 @@ const {
const fs = require('fs');

export type SnapshotStateOptions = {|
shouldUpdate: boolean,
updateSnapshot: SnapshotUpdateState,
snapshotPath?: string,
expand?: boolean,
|};
Expand All @@ -33,7 +33,7 @@ class SnapshotState {
_counters: Map<string, number>;
_dirty: boolean;
_index: number;
_shouldUpdate: boolean;
_updateSnapshot: SnapshotUpdateState;
_snapshotData: {[key: string]: string};
_snapshotPath: Path;
_uncheckedKeys: Set<string>;
Expand All @@ -43,14 +43,11 @@ class SnapshotState {
unmatched: number;
updated: number;

constructor(
testPath: Path,
options: SnapshotStateOptions,
) {
constructor(testPath: Path, options: SnapshotStateOptions) {
this._snapshotPath = options.snapshotPath || getSnapshotPath(testPath);
const {data, dirty} = getSnapshotData(
this._snapshotPath,
options.shouldUpdate,
options.updateSnapshot,
);
this._snapshotData = data;
this._dirty = dirty;
Expand All @@ -61,7 +58,7 @@ class SnapshotState {
this.added = 0;
this.matched = 0;
this.unmatched = 0;
this._shouldUpdate = options.shouldUpdate;
this._updateSnapshot = options.updateSnapshot;
this.updated = 0;
}

Expand All @@ -78,19 +75,18 @@ class SnapshotState {
this._snapshotData[key] = receivedSerialized;
}

save(shouldUpdate: boolean) {
save(shouldUpdate: SnapshotUpdateState) {
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 (shouldUpdate) {
if (shouldUpdate === 'all') {
fs.unlinkSync(this._snapshotPath);
}
status.deleted = true;
Expand Down Expand Up @@ -138,10 +134,11 @@ class SnapshotState {

if (
!fs.existsSync(this._snapshotPath) || // there's no snapshot file
(hasSnapshot && this._shouldUpdate) || // there is a file, but we're updating
!hasSnapshot // there is a file, but it doesn't have this snaphsot
(hasSnapshot && this._updateSnapshot === 'all') || // there is a file, but we're updating
(!hasSnapshot &&
(this._updateSnapshot === 'new' || this._updateSnapshot === 'all')) // there is a file, but it doesn't have this snaphsot
) {
if (this._shouldUpdate) {
if (this._updateSnapshot === 'all') {
if (!pass) {
if (hasSnapshot) {
this.updated++;
Expand Down Expand Up @@ -169,7 +166,7 @@ class SnapshotState {
return {
actual: unescape(receivedSerialized),
count,
expected: unescape(expected),
expected: expected ? unescape(expected) : null,
pass: false,
};
} else {
Expand Down
12 changes: 6 additions & 6 deletions packages/jest-snapshot/src/__tests__/utils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`] = `<div>\n</div>`;\n');
const update = false;
const update = 'none';

expect(() => getSnapshotData(filename, update)).toThrowError(
chalk.red(
Expand All @@ -106,7 +106,7 @@ test('getSnapshotData() throws for older snapshot version', () => {
`// Jest Snapshot v0.99, ${SNAPSHOT_GUIDE_LINK}\n\n` +
'exports[`myKey`] = `<div>\n</div>`;\n',
);
const update = false;
const update = 'none';

expect(() => getSnapshotData(filename, update)).toThrowError(
chalk.red(
Expand All @@ -129,7 +129,7 @@ test('getSnapshotData() throws for newer snapshot version', () => {
`// Jest Snapshot v2, ${SNAPSHOT_GUIDE_LINK}\n\n` +
'exports[`myKey`] = `<div>\n</div>`;\n',
);
const update = false;
const update = 'none';

expect(() => getSnapshotData(filename, update)).toThrowError(
chalk.red(
Expand All @@ -148,15 +148,15 @@ 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`] = `<div>\n</div>`;\n');
const update = true;
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');
fs.readFileSync = jest.fn(() => 'exports[`myKey`] = `<div>\n</div>`;\n');
const update = true;
const update = 'all';

expect(getSnapshotData(filename, update)).toMatchObject({dirty: true});
});
Expand All @@ -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`] = `<div>\n</div>`;\n',
);
const update = true;
const update = 'all';

expect(getSnapshotData(filename, update)).toMatchObject({dirty: false});
});
Expand Down
69 changes: 34 additions & 35 deletions packages/jest-snapshot/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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
Expand All @@ -45,7 +46,7 @@ const cleanup = (hasteFS: HasteFS, update: boolean) => {
),
)
.map(snapshotFile => {
if (update) {
if (update === 'all') {
fs.unlinkSync(snapshotFile);
}
}).length;
Expand All @@ -58,15 +59,7 @@ const cleanup = (hasteFS: HasteFS, update: boolean) => {
const toMatchSnapshot = function(received: any, testName?: string) {
this.dontThrow && this.dontThrow();

const {
currentTestName,
isNot,
snapshotState,
}: {
currentTestName: string,
isNot: boolean,
snapshotState: SnapshotState,
} = this;
const {currentTestName, isNot, snapshotState}: MatcherState = this;

if (isNot) {
throw new Error('Jest: `.not` cannot be used with `.toMatchSnapshot()`.');
Expand All @@ -76,45 +69,51 @@ const toMatchSnapshot = function(received: any, testName?: string) {
throw new Error('Jest: snapshot state must be initialized.');
}

const {actual, expected, count, pass} = snapshotState.match(
testName || currentTestName,
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 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) {
Expand Down
8 changes: 4 additions & 4 deletions packages/jest-snapshot/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -96,7 +96,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;
Expand All @@ -113,11 +113,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;
}

Expand Down
1 change: 1 addition & 0 deletions types/Argv.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type Argv = {|
cache: boolean,
cacheDirectory: string,
clearMocks: boolean,
ci: boolean,
collectCoverage: boolean,
collectCoverageFrom: Array<string>,
collectCoverageOnlyFrom: Array<string>,
Expand Down
Loading

0 comments on commit be3cd57

Please sign in to comment.