Skip to content

Commit

Permalink
feat: Allow custom serializers through setSerializers
Browse files Browse the repository at this point in the history
Provides new setSerializers function to set the serializer objects to
be used before diffing. React component serializer implementation using
`react-test-renderer` has been extracted out.

Fixes jest-community#18
Fixes jest-community#30
Fixes jest-community#31
  • Loading branch information
alistairjcbrown committed Nov 4, 2019
1 parent 8cc8f70 commit 6afc4a0
Show file tree
Hide file tree
Showing 8 changed files with 859 additions and 44 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,45 @@ exports[`snapshot difference between 2 React components state 1`] = `
`;
```

## Custom serializers

By default, `snapshot-diff` uses a built in React serializer based on `react-test-renderer`. The
[serializers](https://jestjs.io/docs/en/configuration#snapshotserializers-array-string) used can be set by calling
`setSerializers` with an array of serializers to use. The order of serializers in this array may be important to you as
serializers are tested in order until a match is found.

`setSerializers` can be used to add new serializers for unsupported data types, or to set a different serializer
for React components. If you want to keep the default React serializer in place, don't forget to add the default
serializers to your list of serializers!

### Adding a new custom serializer

```js
const snapshotDiff = require('snapshot-diff');
const myCustomSerializer = require('./my-custom-serializer');

snapshotDiff.setSerializers([
...snapshotDiff.defaultSerializers, // use default React serializer - add this if you want to serialise React components!
myCustomSerializer
]);
```

### Serializing React components with a different serializer

You can replace the default React serializer by omitting it from the serializer list. The following uses enzymes to-json
serializer instead:

```js
const snapshotDiff = require('snapshot-diff');
const enzymeToJson = require('enzyme-to-json/serializer');
const myCustomSerializer = require('./my-custom-serializer');

snapshotDiff.setSerializers([
enzymeToJson, // using enzymes serializer instead
myCustomSerializer
]);
```

## Snapshot serializer

By default Jest adds extra quotes around strings so it makes diff snapshots of objects too noisy.
Expand Down
107 changes: 107 additions & 0 deletions __tests__/__snapshots__/setSerializers.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`default rendered components can use contextLines 1`] = `
"Snapshot Diff:
- <NestedComponent test=\\"say\\" />
+ <NestedComponent test=\\"my name\\" />
@@ -4,1 +4,1 @@
- say
+ my name"
`;

exports[`default rendered components diffs components 1`] = `
"Snapshot Diff:
- <NestedComponent test=\\"say\\" />
+ <NestedComponent test=\\"my name\\" />
@@ -1,9 +1,9 @@
<div>
<span>
Hello World -
- say
+ my name
</span>
<div>
I have value
1234
</div>"
`;
exports[`enzyme shallow rendered components can use contextLines 1`] = `
"Snapshot Diff:
- First value
+ Second value
@@ -4,1 +4,1 @@
- say
+ my name"
`;
exports[`enzyme shallow rendered components diffs components 1`] = `
"Snapshot Diff:
- First value
+ Second value
<div>
<span>
Hello World -
- say
+ my name
</span>
<Component
value={1234}
/>
</div>"
`;
exports[`react test-renderer shallow rendered components can use contextLines 1`] = `
"Snapshot Diff:
- <div><span>Hello World - say</span><Component value={1234} /></div>
+ <div><span>Hello World - my name</span><Component value={1234} /></div>
@@ -4,1 +4,1 @@
- say
+ my name"
`;
exports[`react test-renderer shallow rendered components diffs components 1`] = `
"Snapshot Diff:
- <div><span>Hello World - say</span><Component value={1234} /></div>
+ <div><span>Hello World - my name</span><Component value={1234} /></div>
@@ -1,9 +1,9 @@
<div>
<span>
Hello World -
- say
+ my name
</span>
<div>
I have value
1234
</div>"
`;
exports[`values which are not components can use contextLines 1`] = `
"Snapshot Diff:
- First value
+ Second value
@@ -3,1 +3,1 @@
- \\"hello\\": \\"world\\",
+ \\"hello\\": \\"there\\","
`;
exports[`values which are not components diffs objects 1`] = `
"Snapshot Diff:
- First value
+ Second value
Object {
\\"foo\\": \\"bar\\",
- \\"hello\\": \\"world\\",
+ \\"hello\\": \\"there\\",
\\"testing\\": 123,
}"
`;
117 changes: 117 additions & 0 deletions __tests__/setSerializers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// @flow
const React = require('react');
const { configure, shallow: enzymeShallow } = require('enzyme');
const ReactShallowRenderer = require('react-test-renderer/shallow');
const Adapter = require('enzyme-adapter-react-16');
const enzymeToJson = require('enzyme-to-json/serializer');
const snapshotDiff = require('../src/index');

configure({ adapter: new Adapter() });
const reactShallow = new ReactShallowRenderer();

snapshotDiff.setSerializers([...snapshotDiff.defaultSerializers, enzymeToJson]);

type Props = {
test: string,
};

const Component = ({ value }) => <div>I have value {value}</div>;

const NestedComponent = (props: Props) => (
<div>
<span>Hello World - {props.test}</span>
<Component value={1234} />
</div>
);

describe('default rendered components', () => {
test('diffs components', () => {
expect(
snapshotDiff(
<NestedComponent test="say" />,
<NestedComponent test="my name" />
)
).toMatchSnapshot();
});

test('can use contextLines', () => {
expect(
snapshotDiff(
<NestedComponent test="say" />,
<NestedComponent test="my name" />,
{
contextLines: 0,
}
)
).toMatchSnapshot();
});
});

describe('enzyme shallow rendered components', () => {
test('diffs components', () => {
expect(
snapshotDiff(
enzymeShallow(<NestedComponent test="say" />),
enzymeShallow(<NestedComponent test="my name" />)
)
).toMatchSnapshot();
});

test('can use contextLines', () => {
expect(
snapshotDiff(
enzymeShallow(<NestedComponent test="say" />),
enzymeShallow(<NestedComponent test="my name" />),
{
contextLines: 0,
}
)
).toMatchSnapshot();
});
});

describe('react test-renderer shallow rendered components', () => {
test('diffs components', () => {
expect(
snapshotDiff(
reactShallow.render(<NestedComponent test="say" />),
reactShallow.render(<NestedComponent test="my name" />)
)
).toMatchSnapshot();
});

test('can use contextLines', () => {
expect(
snapshotDiff(
reactShallow.render(<NestedComponent test="say" />),
reactShallow.render(<NestedComponent test="my name" />),
{
contextLines: 0,
}
)
).toMatchSnapshot();
});
});

describe('values which are not components', () => {
test('diffs objects', () => {
expect(
snapshotDiff(
{ foo: 'bar', hello: 'world', testing: 123 },
{ foo: 'bar', hello: 'there', testing: 123 }
)
).toMatchSnapshot();
});

test('can use contextLines', () => {
expect(
snapshotDiff(
{ foo: 'bar', hello: 'world', testing: 123 },
{ foo: 'bar', hello: 'there', testing: 123 },
{
contextLines: 0,
}
)
).toMatchSnapshot();
});
});
3 changes: 2 additions & 1 deletion extend-expect.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* global expect */
const { toMatchDiffSnapshot } = require('./build/');

expect.extend({ toMatchDiffSnapshot });
expect.extend({ toMatchDiffSnapshot });
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"enzyme-to-json": "^3.4.0",
"eslint": "^5.15.3",
"eslint-config-callstack-io": "^1.1.1",
"flow-bin": "^0.102.0",
"jest": "^24.0.0",
"react": "^16.7.0",
"react-dom": "16.7.0",
"react-test-renderer": "^16.7.0"
}
}
63 changes: 24 additions & 39 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@

const diff = require('jest-diff');
const snapshot = require('jest-snapshot');
const prettyFormat = require('pretty-format');

const { ReactElement } = prettyFormat.plugins;
const reactElement = Symbol.for('react.element');
const reactSerializer = require('./react-serializer');

type Options = {|
expand?: boolean,
Expand All @@ -29,12 +26,27 @@ const defaultOptions = {

const SNAPSHOT_TITLE = 'Snapshot Diff:\n';

const identity = value => value;
const defaultSerializers = [reactSerializer];
let serializers = defaultSerializers;

const snapshotDiff = (valueA: any, valueB: any, options?: Options): string => {
let difference;
const mergedOptions = { ...defaultOptions, ...options };

if (isReactComponent(valueA) && isReactComponent(valueB)) {
difference = diffReactComponents(valueA, valueB, mergedOptions);
const matchingSerializer = serializers.find(
({ test }) => test(valueA) && test(valueB)
);

if (matchingSerializer) {
const { print, diffOptions } = matchingSerializer;
const serializerOptions = diffOptions
? diffOptions(valueA, valueB) || {}
: {};
difference = diffStrings(print(valueA, identity), print(valueB, identity), {
...mergedOptions,
...serializerOptions,
});
} else {
difference = diffStrings(valueA, valueB, mergedOptions);
}
Expand All @@ -55,9 +67,6 @@ const snapshotDiff = (valueA: any, valueB: any, options?: Options): string => {
return SNAPSHOT_TITLE + difference;
};

const isReactComponent = (value: any) =>
value && value.$$typeof === reactElement;

function diffStrings(valueA: any, valueB: any, options: Options) {
return diff(valueA, valueB, {
expand: options.expand,
Expand All @@ -67,36 +76,6 @@ function diffStrings(valueA: any, valueB: any, options: Options) {
});
}

function requireReactTestRenderer() {
try {
return require('react-test-renderer'); // eslint-disable-line import/no-extraneous-dependencies
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
throw new Error(
`Failed to load optional module "react-test-renderer". ` +
`If you need to compare React elements, please add "react-test-renderer" to your ` +
`project's dependencies.\n` +
`${error.message}`
);
}
throw error;
}
}

function diffReactComponents(valueA: any, valueB: any, options: Options) {
const renderer = requireReactTestRenderer();
const reactValueA = renderer.create(valueA).toJSON();
const reactValueB = renderer.create(valueB).toJSON();
const prettyFormatOptions = { plugins: [ReactElement], min: true };

return diff(reactValueA, reactValueB, {
expand: options.expand,
contextLines: options.contextLines,
aAnnotation: prettyFormat(valueA, prettyFormatOptions),
bAnnotation: prettyFormat(valueB, prettyFormatOptions),
});
}

function toMatchDiffSnapshot(
valueA: any,
valueB: any,
Expand All @@ -119,7 +98,13 @@ function getSnapshotDiffSerializer() {
};
}

function setSerializers(customSerializers) {
serializers = customSerializers;
}

module.exports = snapshotDiff;
module.exports.snapshotDiff = snapshotDiff;
module.exports.toMatchDiffSnapshot = toMatchDiffSnapshot;
module.exports.getSnapshotDiffSerializer = getSnapshotDiffSerializer;
module.exports.setSerializers = setSerializers;
module.exports.defaultSerializers = defaultSerializers;
Loading

0 comments on commit 6afc4a0

Please # to comment.