Skip to content

Commit

Permalink
Add resolver for custom snapshots paths (#6143)
Browse files Browse the repository at this point in the history
* Add resolver for snapshot paths

* Remove snapshotResolver from versioned doc

* Simplify resolveSnapshotPath

* Make error feedback more actionable

* Assert test result before snapshot file

Should help troubleshoot failing tests on CI

* Run integration test with correct flags

Same as the base snapshot.test.js

* Add tests for malformed resolver module

* Resolve paths in tests like implementation

To avoid cross-platform mismatches

* Rename integration-tests => e2e

* Fix code review feedback

* Add changelog entry

* Fix review comments for e2e/__tests__/snapshot_resolver.test.js

* Fix prettier error

* Move changelog entry to correct place

* Move up type import above normal imports

* Add consistency check

* Update snapshot_resolver.js
  • Loading branch information
viddo authored and SimenB committed Sep 26, 2018
1 parent 2b216e4 commit 8eefa96
Show file tree
Hide file tree
Showing 32 changed files with 396 additions and 53 deletions.
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: () => {},
};

0 comments on commit 8eefa96

Please sign in to comment.