Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resolver for custom snapshots paths #6143

Merged
merged 17 commits into from Sep 26, 2018
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@
- `[jest-haste-map]` Add `getFileIterator` to `HasteFS` for faster file iteration ([#7010](https://github.com/facebook/jest/pull/7010)).
- `[jest-worker]` [**BREAKING**] Add functionality to call a `setup` method in the worker before the first call and a `teardown` method when ending the farm ([#7014](https://github.com/facebook/jest/pull/7014)).
- `[jest-config]` [**BREAKING**] Set default `notifyMode` to `failure-change` ([#7024](https://github.com/facebook/jest/pull/7024))
- `[jest-snapshot]` Enable configurable snapshot paths ([#6143](https://github.com/facebook/jest/pull/6143))

### Fixes

Expand Down
1 change: 1 addition & 0 deletions TestUtils.js
Expand Up @@ -102,6 +102,7 @@ const DEFAULT_PROJECT_CONFIG: ProjectConfig = {
setupTestFrameworkScriptFile: null,
skipFilter: false,
skipNodeResolution: false,
snapshotResolver: null,
snapshotSerializers: [],
testEnvironment: 'node',
testEnvironmentOptions: {},
Expand Down
23 changes: 23 additions & 0 deletions docs/Configuration.md
Expand Up @@ -642,6 +642,29 @@ If you want this path to be [relative to the root directory of your project](#ro

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.

### `snapshotResolver` [string]

Default: `undefined`

The path to a module that can resolve test<->snapshot path. This config option lets you customize where Jest stores that snapshot files on disk.

Example snapshot resolver module:

```js
// my-snapshot-resolver-module
module.exports = {
// resolves from test to snapshot path
resolveSnapshotPath: (testPath, snapshotExtension) =>
testPath.replace('__tests__', '__snapshots__') + snapshotExtension,

// resolves from snapshot to test path
resolveTestPath: (snapshotFilePath, snapshotExtension) =>
snapshotFilePath
.replace('__snapshots__', '__tests__')
.slice(0, -snapshotExtension.length),
};
```

### `snapshotSerializers` [array<string>]

Default: `[]`
Expand Down
40 changes: 40 additions & 0 deletions e2e/__tests__/snapshot_resolver.test.js
@@ -0,0 +1,40 @@
/**
* @flow
*/
'use strict';

const fs = require('fs');
const path = require('path');
const runJest = require('../runJest');

const snapshotDir = path.resolve(
__dirname,
'../snapshot-resolver/__snapshots__',
);
const snapshotFile = path.resolve(snapshotDir, 'snapshot.test.js.snap');

describe('Custom snapshot resolver', () => {
const cleanup = () => {
if (fs.existsSync(snapshotFile)) {
fs.unlinkSync(snapshotFile);
}
if (fs.existsSync(snapshotDir)) {
fs.rmdirSync(snapshotDir);
}
};

beforeEach(cleanup);
afterAll(cleanup);

it('Resolves snapshot files using custom resolver', () => {
const result = runJest('snapshot-resolver', ['-w=1', '--ci=false']);

expect(result.stderr).toMatch('1 snapshot written from 1 test suite');

// $FlowFixMe dynamic require
const content = require(snapshotFile);
expect(content).toHaveProperty(
'snapshots are written to custom location 1',
);
});
});
3 changes: 3 additions & 0 deletions e2e/snapshot-resolver/__tests__/snapshot.test.js
@@ -0,0 +1,3 @@
test('snapshots are written to custom location', () => {
expect('foobar').toMatchSnapshot();
});
9 changes: 9 additions & 0 deletions e2e/snapshot-resolver/customSnapshotResolver.js
@@ -0,0 +1,9 @@
module.exports = {
resolveSnapshotPath: (testPath, snapshotExtension) =>
testPath.replace('__tests__', '__snapshots__') + snapshotExtension,

resolveTestPath: (snapshotFilePath, snapshotExtension) =>
snapshotFilePath
.replace('__snapshots__', '__tests__')
.slice(0, -snapshotExtension.length),
};
6 changes: 6 additions & 0 deletions e2e/snapshot-resolver/package.json
@@ -0,0 +1,6 @@
{
"jest": {
"testEnvironment": "node",
"snapshotResolver": "<rootDir>/customSnapshotResolver.js"
}
}
1 change: 1 addition & 0 deletions jest.config.js
Expand Up @@ -42,6 +42,7 @@ module.exports = {
'/packages/jest-runtime/src/__tests__/module_dir/',
'/packages/jest-runtime/src/__tests__/NODE_PATH_dir',
'/packages/jest-snapshot/src/__tests__/plugins',
'/packages/jest-snapshot/src/__tests__/fixtures/',
'/packages/jest-validate/src/__tests__/fixtures/',
'/packages/jest-worker/src/__performance_tests__',
'/packages/pretty-format/perf/test.js',
Expand Down
Expand Up @@ -13,7 +13,11 @@ import type {Event, TestEntry} from 'types/Circus';

import {extractExpectedAssertionsErrors, getState, setState} from 'expect';
import {formatExecError, formatResultsErrors} from 'jest-message-util';
import {SnapshotState, addSerializer} from 'jest-snapshot';
import {
SnapshotState,
addSerializer,
buildSnapshotResolver,
} from 'jest-snapshot';
import {addEventHandler, dispatch, ROOT_DESCRIBE_BLOCK_NAME} from '../state';
import {getTestID, getOriginalPromise} from '../utils';
import run from '../run';
Expand Down Expand Up @@ -96,7 +100,9 @@ export const initialize = ({
});

const {expand, updateSnapshot} = globalConfig;
const snapshotState = new SnapshotState(testPath, {
const snapshotResolver = buildSnapshotResolver(config);
const snapshotPath = snapshotResolver.resolveSnapshotPath(testPath);
const snapshotState = new SnapshotState(snapshotPath, {
expand,
getBabelTraverse,
getPrettier,
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-cli/src/SearchSource.js
Expand Up @@ -18,6 +18,7 @@ import DependencyResolver from 'jest-resolve-dependencies';
import testPathPatternToRegExp from './testPathPatternToRegexp';
import {escapePathForRegex} from 'jest-regex-util';
import {replaceRootDirInPath} from 'jest-config';
import {buildSnapshotResolver} from 'jest-snapshot';

type SearchResult = {|
noSCM?: boolean,
Expand Down Expand Up @@ -153,6 +154,7 @@ export default class SearchSource {
const dependencyResolver = new DependencyResolver(
this._context.resolver,
this._context.hasteFS,
buildSnapshotResolver(this._context.config),
);

const tests = toTests(
Expand Down
1 change: 1 addition & 0 deletions packages/jest-cli/src/TestScheduler.js
Expand Up @@ -161,6 +161,7 @@ export default class TestScheduler {
const status = snapshot.cleanup(
context.hasteFS,
this._globalConfig.updateSnapshot,
snapshot.buildSnapshotResolver(context.config),
);

aggregatedResults.snapshot.filesRemoved += status.filesRemoved;
Expand Down
4 changes: 2 additions & 2 deletions packages/jest-cli/src/lib/is_valid_path.js
Expand Up @@ -8,7 +8,7 @@
*/

import type {GlobalConfig, ProjectConfig} from 'types/Config';
import Snapshot from 'jest-snapshot';
import {isSnapshotPath} from 'jest-snapshot';

export default function isValidPath(
globalConfig: GlobalConfig,
Expand All @@ -18,6 +18,6 @@ export default function isValidPath(
return (
!filePath.includes(globalConfig.coverageDirectory) &&
!config.watchPathIgnorePatterns.some(pattern => filePath.match(pattern)) &&
!filePath.endsWith(`.${Snapshot.EXTENSION}`)
!isSnapshotPath(filePath)
);
}
1 change: 1 addition & 0 deletions packages/jest-config/src/ValidConfig.js
Expand Up @@ -91,6 +91,7 @@ export default ({
silent: true,
skipFilter: false,
skipNodeResolution: false,
snapshotResolver: '<rootDir>/snapshotResolver.js',
snapshotSerializers: ['my-serializer-module'],
testEnvironment: 'jest-environment-jsdom',
testEnvironmentOptions: {userAgent: 'Agent/007'},
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/index.js
Expand Up @@ -185,6 +185,7 @@ const getConfigs = (
setupTestFrameworkScriptFile: options.setupTestFrameworkScriptFile,
skipFilter: options.skipFilter,
skipNodeResolution: options.skipNodeResolution,
snapshotResolver: options.snapshotResolver,
snapshotSerializers: options.snapshotSerializers,
testEnvironment: options.testEnvironment,
testEnvironmentOptions: options.testEnvironmentOptions,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/normalize.js
Expand Up @@ -453,6 +453,7 @@ export default function normalize(options: InitialOptions, argv: Argv) {
case 'moduleLoader':
case 'runner':
case 'setupTestFrameworkScriptFile':
case 'snapshotResolver':
case 'testResultsProcessor':
case 'testRunner':
case 'filter':
Expand Down
16 changes: 13 additions & 3 deletions packages/jest-editor-support/src/Snapshot.js
Expand Up @@ -10,9 +10,11 @@

'use strict';

import type {ProjectConfig} from 'types/Config';

import traverse from 'babel-traverse';
import {getASTfor} from './parsers/babylon_parser';
import {utils} from 'jest-snapshot';
import {buildSnapshotResolver, utils} from 'jest-snapshot';

type Node = any;

Expand Down Expand Up @@ -95,11 +97,17 @@ const buildName: (
export default class Snapshot {
_parser: Function;
_matchers: Array<string>;
constructor(parser: any, customMatchers?: Array<string>) {
_projectConfig: ?ProjectConfig;
constructor(
parser: any,
customMatchers?: Array<string>,
projectConfig?: ProjectConfig,
) {
this._parser = parser || getASTfor;
this._matchers = ['toMatchSnapshot', 'toThrowErrorMatchingSnapshot'].concat(
customMatchers || [],
);
this._projectConfig = projectConfig;
}

getMetadata(filePath: string): Array<SnapshotMetadata> {
Expand Down Expand Up @@ -127,7 +135,9 @@ export default class Snapshot {
},
});

const snapshotPath = utils.getSnapshotPath(filePath);
// NOTE if no projectConfig is given the default resolver will be used
const snapshotResolver = buildSnapshotResolver(this._projectConfig || {});
const snapshotPath = snapshotResolver.resolveSnapshotPath(filePath);
const snapshots = utils.getSnapshotData(snapshotPath, 'none').data;
let lastParent = null;
let count = 1;
Expand Down
11 changes: 9 additions & 2 deletions packages/jest-jasmine2/src/setup_jest_globals.js
Expand Up @@ -11,7 +11,11 @@ import type {GlobalConfig, Path, ProjectConfig} from 'types/Config';
import type {Plugin} from 'types/PrettyFormat';

import {extractExpectedAssertionsErrors, getState, setState} from 'expect';
import {SnapshotState, addSerializer} from 'jest-snapshot';
import {
buildSnapshotResolver,
SnapshotState,
addSerializer,
} from 'jest-snapshot';

export type SetupOptions = {|
config: ProjectConfig,
Expand Down Expand Up @@ -96,9 +100,12 @@ export default ({
.forEach(path => {
addSerializer(localRequire(path));
});

patchJasmine();
const {expand, updateSnapshot} = globalConfig;
const snapshotState = new SnapshotState(testPath, {
const snapshotResolver = buildSnapshotResolver(config);
const snapshotPath = snapshotResolver.resolveSnapshotPath(testPath);
const snapshotState = new SnapshotState(snapshotPath, {
expand,
getBabelTraverse: () => require('babel-traverse').default,
getPrettier: () =>
Expand Down
Expand Up @@ -9,6 +9,7 @@

const path = require('path');
const {normalize} = require('jest-config');
const {buildSnapshotResolver} = require('jest-snapshot');
const DependencyResolver = require('../index');

const maxWorkers = 1;
Expand All @@ -34,6 +35,7 @@ beforeEach(() => {
dependencyResolver = new DependencyResolver(
hasteMap.resolver,
hasteMap.hasteFS,
buildSnapshotResolver(config),
);
});
});
Expand Down
24 changes: 10 additions & 14 deletions packages/jest-resolve-dependencies/src/index.js
Expand Up @@ -10,16 +10,8 @@
import type {HasteFS} from 'types/HasteMap';
import type {Path} from 'types/Config';
import type {Resolver, ResolveModuleConfig} from 'types/Resolve';
import Snapshot from 'jest-snapshot';

import {replacePathSepForRegex} from 'jest-regex-util';

const snapshotDirRegex = new RegExp(replacePathSepForRegex('/__snapshots__/'));
const snapshotFileRegex = new RegExp(
replacePathSepForRegex(`__snapshots__/(.*).${Snapshot.EXTENSION}`),
);
const isSnapshotPath = (path: string): boolean =>
!!path.match(snapshotDirRegex);
import type {SnapshotResolver} from 'types/SnapshotResolver';
import {isSnapshotPath} from 'jest-snapshot';

/**
* DependencyResolver is used to resolve the direct dependencies of a module or
Expand All @@ -28,10 +20,16 @@ const isSnapshotPath = (path: string): boolean =>
class DependencyResolver {
_hasteFS: HasteFS;
_resolver: Resolver;
_snapshotResolver: SnapshotResolver;

constructor(resolver: Resolver, hasteFS: HasteFS) {
constructor(
resolver: Resolver,
hasteFS: HasteFS,
snapshotResolver: SnapshotResolver,
) {
this._resolver = resolver;
this._hasteFS = hasteFS;
this._snapshotResolver = snapshotResolver;
}

resolve(file: Path, options?: ResolveModuleConfig): Array<Path> {
Expand Down Expand Up @@ -89,10 +87,8 @@ class DependencyResolver {
const changed = new Set();
for (const path of paths) {
if (this._hasteFS.exists(path)) {
// /path/to/__snapshots__/test.js.snap is always adjacent to
// /path/to/test.js
const modulePath = isSnapshotPath(path)
? path.replace(snapshotFileRegex, '$1')
? this._snapshotResolver.resolveTestPath(path)
: path;
changed.add(modulePath);
if (filter(modulePath)) {
Expand Down
6 changes: 2 additions & 4 deletions packages/jest-snapshot/src/State.js
Expand Up @@ -14,7 +14,6 @@ import {getTopFrame, getStackTraceLines} from 'jest-message-util';
import {
saveSnapshotFile,
getSnapshotData,
getSnapshotPath,
keyToTestName,
serialize,
testNameToKey,
Expand All @@ -26,7 +25,6 @@ export type SnapshotStateOptions = {|
updateSnapshot: SnapshotUpdateState,
getPrettier: () => null | any,
getBabelTraverse: () => Function,
snapshotPath?: string,
expand?: boolean,
|};

Expand Down Expand Up @@ -55,8 +53,8 @@ export default class SnapshotState {
unmatched: number;
updated: number;

constructor(testPath: Path, options: SnapshotStateOptions) {
this._snapshotPath = options.snapshotPath || getSnapshotPath(testPath);
constructor(snapshotPath: Path, options: SnapshotStateOptions) {
this._snapshotPath = snapshotPath;
const {data, dirty} = getSnapshotData(
this._snapshotPath,
options.updateSnapshot,
Expand Down
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`malformed custom resolver in project config inconsistent functions throws 1`] = `"<bold>Custom snapshot resolver functions must transform paths consistently, i.e. expects resolveTestPath(resolveSnapshotPath('some-path/__tests__/snapshot_resolver.test.js')) === some-path/__SPECS__/snapshot_resolver.test.js</>"`;

exports[`malformed custom resolver in project config missing resolveSnapshotPath throws 1`] = `
"<bold>Custom snapshot resolver must implement a \`resolveSnapshotPath\` function.</>
Documentation: https://facebook.github.io/jest/docs/en/configuration.html#snapshotResolver"
`;

exports[`malformed custom resolver in project config missing resolveTestPath throws 1`] = `
"<bold>Custom snapshot resolver must implement a \`resolveTestPath\` function.</>
Documentation: https://facebook.github.io/jest/docs/en/configuration.html#snapshotResolver"
`;
@@ -0,0 +1,9 @@
module.exports = {
resolveSnapshotPath: (testPath, snapshotExtension) =>
testPath.replace('__tests__', '__snapshots__') + snapshotExtension,

resolveTestPath: (snapshotFilePath, snapshotExtension) =>
snapshotFilePath
.replace('__snapshots__', '__SPECS__')
.slice(0, -snapshotExtension.length),
};
@@ -0,0 +1,3 @@
module.exports = {
resolveTestPath: () => {},
};
@@ -0,0 +1,3 @@
module.exports = {
resolveSnapshotPath: () => {},
};