diff --git a/docs/Configuration.md b/docs/Configuration.md index 57b9f7ba2aab..44ed8d8e1ce0 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -63,6 +63,7 @@ These options let you control Jest's behavior in your `package.json` file. The J - [`scriptPreprocessor` [string]](#scriptpreprocessor-string) - [`setupFiles` [array]](#setupfiles-array) - [`setupTestFrameworkScriptFile` [string]](#setuptestframeworkscriptfile-string) + - [`snapshotSerializers` [array]](#snapshotserializers-array-string) - [`testEnvironment` [string]](#testenvironment-string) - [`testPathDirs` [array]](#testpathdirs-array-string) - [`testPathIgnorePatterns` [array]](#testpathignorepatterns-array-string) @@ -298,6 +299,63 @@ The path to a module that runs some code to configure or set up the testing fram For example, Jest ships with several plug-ins to `jasmine` that work by monkey-patching the jasmine API. If you wanted to add even more jasmine plugins to the mix (or if you wanted some custom, project-wide matchers for example), you could do so in this module. +### `snapshotSerializers` [array] +(default: `[]`) + +A list of paths to snapshot serializer modules Jest should use for snapshot +testing. + +Jest has default serializers for built-in javascript types and for react +elements. See [snapshot test tutorial](/jest/docs/tutorial-react-native.html#snapshot-test) for more information. + +Example serializer module: + +```js +// my-serializer-module +module.exports = { + test: function(val) { + return val && val.hasOwnProperty('foo'); + }, + print: function(val, serialize, indent) { + return 'Pretty foo: ' + serialize(val.foo); + } +}; +``` + +`serialize` is a function that serializes a value using existing plugins. + +To use `my-serializer-module` as a serializer, configuration would be as +follows: + +```json +{ + "json": { + "snapshotSerializers": ["/node_modules/my-serializer-module"] + } +} +``` + +Finally tests would look as follows: + +```js +test(() => { + const bar = { + foo: {x: 1, y: 2} + }; + + expect(foo).toMatchSnapshot(); +}); +``` + +Rendered snapshot: + +``` +Pretty foo: Object { + "x": 1, + "y": 2, +} +``` + ### `testEnvironment` [string] (default: `'jsdom'`) diff --git a/integration_tests/__tests__/__snapshots__/snapshot-serializers-test.js.snap b/integration_tests/__tests__/__snapshots__/snapshot-serializers-test.js.snap new file mode 100644 index 000000000000..b33ad539c346 --- /dev/null +++ b/integration_tests/__tests__/__snapshots__/snapshot-serializers-test.js.snap @@ -0,0 +1,7 @@ +exports[`Snapshot serializers renders snapshot 1`] = ` +Object { + "snapshot serializers works with first plugin 1": "foo: 1", + "snapshot serializers works with nested serializable objects 1": "foo: bar: 2", + "snapshot serializers works with second plugin 1": "bar: 2", +} +`; diff --git a/integration_tests/__tests__/snapshot-serializers-test.js b/integration_tests/__tests__/snapshot-serializers-test.js new file mode 100644 index 000000000000..d04f3c2ea0d2 --- /dev/null +++ b/integration_tests/__tests__/snapshot-serializers-test.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +'use strict'; + +const path = require('path'); +const skipOnWindows = require('skipOnWindows'); +const {cleanup} = require('../utils'); +const runJest = require('../runJest'); + +const testDir = path.resolve(__dirname, '../snapshot-serializers'); +const snapshotsDir = path.resolve(testDir, '__tests__/__snapshots__'); +const snapshotPath = path.resolve(snapshotsDir, 'snapshot-test.js.snap'); + +const runAndAssert = () => { + const result = runJest.json('snapshot-serializers'); + const json = result.json; + expect(json.numTotalTests).toBe(3); + expect(json.numPassedTests).toBe(3); + expect(json.numFailedTests).toBe(0); + expect(json.numPendingTests).toBe(0); + expect(result.status).toBe(0); +}; + +describe('Snapshot serializers', () => { + skipOnWindows.suite(); + + beforeEach(() => cleanup(snapshotsDir)); + afterEach(() => cleanup(snapshotsDir)); + + it('renders snapshot', () => { + runAndAssert(); + const snapshot = require(snapshotPath); + expect(snapshot).toMatchSnapshot(); + }); + + it('compares snapshots correctly', () => { + // run twice, second run compares result with snapshot from first run + runAndAssert(); + runAndAssert(); + }); +}); diff --git a/integration_tests/snapshot-serializers/__tests__/snapshot-test.js b/integration_tests/snapshot-serializers/__tests__/snapshot-test.js new file mode 100644 index 000000000000..0eed05a23c52 --- /dev/null +++ b/integration_tests/snapshot-serializers/__tests__/snapshot-test.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +'use strict'; + +describe('snapshot serializers', () => { + it('works with first plugin', () => { + const test = { + foo: 1, + }; + expect(test).toMatchSnapshot(); + }); + + it('works with second plugin', () => { + const test = { + bar: 2, + }; + expect(test).toMatchSnapshot(); + }); + + it('works with nested serializable objects', () => { + const test = { + foo: { + bar: 2, + }, + }; + expect(test).toMatchSnapshot(); + }); +}); diff --git a/integration_tests/snapshot-serializers/package.json b/integration_tests/snapshot-serializers/package.json new file mode 100644 index 000000000000..1577ef57dea1 --- /dev/null +++ b/integration_tests/snapshot-serializers/package.json @@ -0,0 +1,9 @@ +{ + "jest": { + "testEnvironment": "node", + "snapshotSerializers": [ + "/plugins/foo", + "/plugins/bar" + ] + } +} diff --git a/integration_tests/snapshot-serializers/plugins/bar.js b/integration_tests/snapshot-serializers/plugins/bar.js new file mode 100644 index 000000000000..0720054dbe67 --- /dev/null +++ b/integration_tests/snapshot-serializers/plugins/bar.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +'use strict'; + +const createPlugin = require('../utils').createPlugin; +module.exports = createPlugin('bar'); diff --git a/integration_tests/snapshot-serializers/plugins/foo.js b/integration_tests/snapshot-serializers/plugins/foo.js new file mode 100644 index 000000000000..479700a65bd8 --- /dev/null +++ b/integration_tests/snapshot-serializers/plugins/foo.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +'use strict'; + +const createPlugin = require('../utils').createPlugin; +module.exports = createPlugin('foo'); diff --git a/integration_tests/snapshot-serializers/utils.js b/integration_tests/snapshot-serializers/utils.js new file mode 100644 index 000000000000..f479815926bf --- /dev/null +++ b/integration_tests/snapshot-serializers/utils.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +'use strict'; + +exports.createPlugin = prop => { + return { + test: val => val && val.hasOwnProperty(prop), + print: (val, serialize) => `${prop}: ${serialize(val[prop])}`, + }; +}; diff --git a/packages/jest-config/src/__tests__/normalize-test.js b/packages/jest-config/src/__tests__/normalize-test.js index f621ceac2052..d6122def5b93 100644 --- a/packages/jest-config/src/__tests__/normalize-test.js +++ b/packages/jest-config/src/__tests__/normalize-test.js @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @emails oncall+jsinfra */ 'use strict'; @@ -164,17 +163,17 @@ describe('normalize', () => { }); }); - describe('testPathDirs', () => { + function testPathArray(key) { it('normalizes all paths relative to rootDir', () => { const config = normalize({ rootDir: '/root/path/foo', - testPathDirs: [ + [key]: [ 'bar/baz', 'qux/quux/', ], }, '/root/path'); - expect(config.testPathDirs).toEqual([ + expect(config[key]).toEqual([ expectedPathFooBar, expectedPathFooQux, ]); }); @@ -182,13 +181,13 @@ describe('normalize', () => { it('does not change absolute paths', () => { const config = normalize({ rootDir: '/root/path/foo', - testPathDirs: [ + [key]: [ '/an/abs/path', '/another/abs/path', ], }); - expect(config.testPathDirs).toEqual([ + expect(config[key]).toEqual([ expectedPathAbs, expectedPathAbsAnother, ]); }); @@ -196,13 +195,21 @@ describe('normalize', () => { it('substitutes tokens', () => { const config = normalize({ rootDir: '/root/path/foo', - testPathDirs: [ + [key]: [ '/bar/baz', ], }); - expect(config.testPathDirs).toEqual([expectedPathFooBar]); + expect(config[key]).toEqual([expectedPathFooBar]); }); + } + + describe('testPathDirs', () => { + testPathArray('testPathDirs'); + }); + + describe('snapshotSerializers', () => { + testPathArray('snapshotSerializers'); }); describe('scriptPreprocessor', () => { diff --git a/packages/jest-config/src/defaults.js b/packages/jest-config/src/defaults.js index b4049577deb9..5f1312e14f4c 100644 --- a/packages/jest-config/src/defaults.js +++ b/packages/jest-config/src/defaults.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant @@ -46,6 +46,7 @@ module.exports = ({ preset: null, preprocessorIgnorePatterns: [NODE_MODULES_REGEXP], resetModules: false, + snapshotSerializers: [], testEnvironment: 'jest-environment-jsdom', testPathDirs: [''], testPathIgnorePatterns: [NODE_MODULES_REGEXP], diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index 03f59af50eca..bd88995f1173 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -1,9 +1,10 @@ /** - * Copyright (c) 2014, Facebook, Inc. All rights reserved. + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. + * */ 'use strict'; @@ -314,6 +315,12 @@ function normalize(config, argv) { )); break; + case 'snapshotSerializers': + value = config[key].map(filePath => path.resolve( + config.rootDir, + _replaceRootDirTags(config.rootDir, filePath), + )); + break; case 'collectCoverageFrom': if (!config[key]) { value = []; diff --git a/packages/jest-jasmine2/src/__mocks__/jest-snapshot.js b/packages/jest-jasmine2/src/__mocks__/jest-snapshot.js new file mode 100644 index 000000000000..eed18eecf5a4 --- /dev/null +++ b/packages/jest-jasmine2/src/__mocks__/jest-snapshot.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +const snapshot = jest.genMockFromModule('jest-snapshot'); +let plugins = []; + +snapshot.addPlugins = p => { + plugins = plugins.concat(p); +}; +snapshot.getPlugins = p => plugins; +snapshot.__reset = () => plugins = []; + +module.exports = snapshot; diff --git a/packages/jest-jasmine2/src/__tests__/setup-jest-globals-test.js b/packages/jest-jasmine2/src/__tests__/setup-jest-globals-test.js new file mode 100644 index 000000000000..b1549db27d7a --- /dev/null +++ b/packages/jest-jasmine2/src/__tests__/setup-jest-globals-test.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +'use strict'; + +describe('addPlugins', () => { + const setup = require('../setup-jest-globals'); + + beforeEach(() => { + require('jest-snapshot').__reset(); + }); + + const test = serializers => { + const {getPlugins} = require('jest-snapshot'); + const config = { + snapshotSerializers: [], + }; + setup({config}); + expect(getPlugins()).toEqual(config.snapshotSerializers); + }; + + it('should add plugins from an empty array', () => test([])); + it('should add a single plugin', () => test(['foo'])); + it('should add multiple plugins', () => test(['foo', 'bar'])); +}); diff --git a/packages/jest-jasmine2/src/setup-jest-globals.js b/packages/jest-jasmine2/src/setup-jest-globals.js index 7bf23c449793..505b4cf9836f 100644 --- a/packages/jest-jasmine2/src/setup-jest-globals.js +++ b/packages/jest-jasmine2/src/setup-jest-globals.js @@ -13,7 +13,7 @@ import type {Config, Path} from 'types/Config'; const {getState, setState} = require('jest-matchers'); -const {initializeSnapshotState} = require('jest-snapshot'); +const {initializeSnapshotState, addPlugins} = require('jest-snapshot'); // Get suppressed errors form jest-matchers that weren't throw during // test execution and add them to the test result, potentially failing @@ -69,6 +69,7 @@ type Options = { }; module.exports = ({testPath, config}: Options) => { + addPlugins(config.snapshotSerializers); setState({testPath}); patchJasmine(); const snapshotState diff --git a/packages/jest-snapshot/src/__tests__/plugins-test.js b/packages/jest-snapshot/src/__tests__/plugins-test.js new file mode 100644 index 000000000000..36941556fda2 --- /dev/null +++ b/packages/jest-snapshot/src/__tests__/plugins-test.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +'use strict'; + +beforeEach(() => jest.resetModules()); + +const testPath = serializers => { + const {addPlugins, getPlugins} = require('../plugins'); + const serializerPaths = serializers.map(s => + require.resolve(`./plugins/${s}`), + ); + addPlugins(serializerPaths); + const expected = serializerPaths.map(p => require(p)); + + const plugins = getPlugins(); + expect(plugins.length).toBe(serializers.length + 2); + plugins.splice(0, 2); + expect(plugins).toEqual(expected); +}; + +it('should get plugins', () => { + const {getPlugins} = require('../plugins'); + const plugins = getPlugins(); + expect(plugins.length).toBe(2); +}); + +it('should add plugins from an empty array', () => testPath([])); +it('should add a single plugin path', () => testPath(['foo'])); +it('should add multiple plugin paths', () => testPath(['foo', 'bar'])); diff --git a/packages/jest-snapshot/src/__tests__/plugins/bar.js b/packages/jest-snapshot/src/__tests__/plugins/bar.js new file mode 100644 index 000000000000..a5330ce174ac --- /dev/null +++ b/packages/jest-snapshot/src/__tests__/plugins/bar.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +'use strict'; + +module.exports = Symbol(); diff --git a/packages/jest-snapshot/src/__tests__/plugins/foo.js b/packages/jest-snapshot/src/__tests__/plugins/foo.js new file mode 100644 index 000000000000..a5330ce174ac --- /dev/null +++ b/packages/jest-snapshot/src/__tests__/plugins/foo.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ +'use strict'; + +module.exports = Symbol(); diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index 51108f4c063f..17fbf48b51db 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -17,6 +17,8 @@ const fileExists = require('jest-file-exists'); const fs = require('fs'); const path = require('path'); const SnapshotState = require('./State'); +const {getPlugins, addPlugins} = require('./plugins'); + const { EXPECTED_COLOR, ensureNoExpected, @@ -144,4 +146,6 @@ module.exports = { initializeSnapshotState, toMatchSnapshot, toThrowErrorMatchingSnapshot, + getPlugins, + addPlugins, }; diff --git a/packages/jest-snapshot/src/plugins.js b/packages/jest-snapshot/src/plugins.js new file mode 100644 index 000000000000..159f8507bb05 --- /dev/null +++ b/packages/jest-snapshot/src/plugins.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ +'use strict'; + +import type {Path} from 'types/Config'; + +const ReactElementPlugin = require('pretty-format/plugins/ReactElement'); +const ReactTestComponentPlugin = require('pretty-format/plugins/ReactTestComponent'); + +let PLUGINS = [ReactElementPlugin, ReactTestComponentPlugin]; + +exports.addPlugins = (plugins: Array) => { + PLUGINS = PLUGINS.concat(plugins.map(p => { + /* $FlowFixMe */ + return require(p); + })); +}; + +exports.getPlugins = () => PLUGINS; diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index fd9e55577f91..573be692d728 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -16,12 +16,10 @@ const createDirectory = require('jest-util').createDirectory; const fileExists = require('jest-file-exists'); const path = require('path'); const prettyFormat = require('pretty-format'); -const ReactElementPlugin = require('pretty-format/plugins/ReactElement'); const fs = require('fs'); const naturalCompare = require('natural-compare'); -const ReactTestComponentPlugin = require('pretty-format/plugins/ReactTestComponent'); +const getPlugins = require('./plugins').getPlugins; -const PLUGINS = [ReactElementPlugin, ReactTestComponentPlugin]; const SNAPSHOT_EXTENSION = 'snap'; const testNameToKey = (testName: string, count: number) => @@ -62,7 +60,7 @@ const addExtraLineBreaks = const serialize = (data: any): string => { return addExtraLineBreaks(prettyFormat(data, { - plugins: PLUGINS, + plugins: getPlugins(), printFunctionName: false, })); }; diff --git a/types/Config.js b/types/Config.js index 71b3a07943cf..ab0c88596882 100644 --- a/types/Config.js +++ b/types/Config.js @@ -40,6 +40,7 @@ export type DefaultConfig = {| preprocessorIgnorePatterns: Array, preset: ?string, resetModules: boolean, + snapshotSerializers: Array, testEnvironment: string, testPathDirs: Array, testPathIgnorePatterns: Array,