Created
April 4, 2017 23:29
-
-
Save 0x24a537r9/9bc7b40aab1ef5a6860db42ce9cf06b4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @providesModule diffReact | |
* @flow | |
*/ | |
import React from 'react'; | |
import {diffLines} from 'diff'; | |
import deepEqual from 'deep-equal'; | |
import invariant from 'invariant'; | |
import prettyFormat from 'pretty-format'; | |
import reactTestPlugin from 'pretty-format/build/plugins/ReactTestComponent'; | |
// NOTE: test renderer must be required after react-native. | |
import renderer from 'react-test-renderer'; | |
// A React POJO (Plain Old Javascript Object), generated by calling | |
// ReactTestComponent.toJSON() (which actually creates a POJO, not JSON as the | |
// name would suggest). | |
type ReactPOJO = | |
| string | |
| { | |
$$typeof?: Symbol, | |
children: ?Array<ReactPOJO>, | |
props: {[key: string]: any}, | |
type: string, | |
}; | |
type CollapseResult = { | |
areEqual: boolean, | |
base: ReactPOJO, | |
test: ReactPOJO, | |
}; | |
// In canonicalize(), we'll want to replace all function props with references | |
// to the same function so that deepEqual() doesn't identify them as different | |
// (since we really only want a diff of the visual props). | |
const SENTINEL_FUNCTION = () => {}; | |
export default function diffReact( | |
base: React.Element<any>, | |
test: React.Element<any>, | |
): string { | |
// Recurse through the two rendered JSON DOMs in parallel, replacing equal | |
// props with a single `…="…"` prop and equal children with `…`. | |
const { | |
areEqual, | |
base: collapsedBase, | |
test: collapsedTest, | |
} = collapseReactPOJOs( | |
canonicalize(renderer.create(base).toJSON()), | |
canonicalize(renderer.create(test).toJSON()), | |
); | |
if (areEqual) { | |
return '(No visual differences)'; | |
} | |
// Perform a standard line-based diff on the formatted, collapsed DOMs. | |
const diffs = diffLines( | |
formatReactPOJO(collapsedBase), | |
formatReactPOJO(collapsedTest), | |
); | |
// Format the diffs in the standard line-prefix (+, -, or ' ') diff format. | |
return diffs | |
.map(({added, removed, value}) => | |
value | |
.replace(/[\r\n]+$/, '') // Strip any final newlines. | |
.split('\n') | |
.map(line => `${added ? '+' : removed ? '-' : ' '} ${line}`) | |
.join('\n')) | |
.join('\n'); | |
} | |
function canonicalize(data: any): any { | |
if (typeof data === 'function') { | |
return SENTINEL_FUNCTION; | |
} else if (Array.isArray(data)) { | |
return data.map(canonicalize); | |
} else if (data instanceof Object) { | |
const canonicalizedData = {}; | |
for (const key in data) { | |
canonicalizedData[key] = canonicalize(data[key]); | |
} | |
return canonicalizedData; | |
} else { | |
return data; | |
} | |
} | |
function collapseReactPOJOs(base: ReactPOJO, test: ReactPOJO): CollapseResult { | |
if (typeof base === 'string' && typeof test === 'string') { | |
return {base, test, areEqual: base === test}; | |
} else if ( | |
typeof base === 'string' || | |
typeof test === 'string' || | |
base.type !== test.type | |
) { | |
return {base, test, areEqual: false}; | |
} | |
const allPropsAreEqual = deepEqual(base.props, test.props); | |
const newBaseProps = {...base.props}; | |
const newTestProps = {...test.props}; | |
for (const prop of Object.keys(base.props)) { | |
if (deepEqual(base.props[prop], test.props[prop])) { | |
// Delete the common props... | |
delete newBaseProps[prop]; | |
delete newTestProps[prop]; | |
// ... and replace them with a single '(equivalent props)="..."' prop. | |
newBaseProps['…'] = '…'; | |
newTestProps['…'] = '…'; | |
} | |
} | |
const allChildrenAreEqual = deepEqual(base.children, test.children); | |
let newBaseChildren = []; | |
let newTestChildren = []; | |
if (allChildrenAreEqual) { | |
if (base.children && base.children.length > 0) { | |
newBaseChildren.push('…'); | |
newTestChildren.push('…'); | |
} | |
} else { | |
const maxEqualChildren = base.children && test.children | |
? Math.min(base.children.length, test.children.length) | |
: 0; | |
let ii; | |
for (ii = 0; ii < maxEqualChildren; ++ii) { | |
invariant( | |
base.children && test.children, | |
'base and test should both have children if in this loop', | |
); | |
const {base: newBaseChild, test: newTestChild} = collapseReactPOJOs( | |
base.children[ii], | |
test.children[ii], | |
); | |
newBaseChildren[ii] = newBaseChild; | |
newTestChildren[ii] = newTestChild; | |
} | |
newBaseChildren = newBaseChildren.concat((base.children || []).slice(ii)); | |
newTestChildren = newTestChildren.concat((test.children || []).slice(ii)); | |
} | |
return { | |
areEqual: allPropsAreEqual && allChildrenAreEqual, | |
base: { | |
type: base.type, | |
props: newBaseProps, | |
children: newBaseChildren.length > 0 ? newBaseChildren : null, | |
}, | |
test: { | |
type: test.type, | |
props: newTestProps, | |
children: newTestChildren.length > 0 ? newTestChildren : null, | |
}, | |
}; | |
} | |
function formatReactPOJO(reactPOJO: ReactPOJO) { | |
if (typeof reactPOJO === 'object') { | |
// HACK: Trick reactTestPlugin into thinking a ReactPOJO is a | |
// ReactTestComponent instance (which has the same type signature as a | |
// ReactPOJO). | |
reactPOJO.$$typeof = Symbol.for('react.test.json'); | |
} | |
const prettyFormatted = prettyFormat(reactPOJO, { | |
escapeRegex: true, | |
plugins: [reactTestPlugin], | |
printFunctionName: false, | |
}); | |
const prettyFormattedAndCollapsed = prettyFormatted | |
// Collapse tags with equal props and equal children like: | |
// <View …="…">…</View> | |
.replace( | |
/(\s*)<(\w+)\n\1 {2}…="…"\n\1>\n\1 {2}…\n\1<\/\2>/g, | |
'$1<$2 …="…">…</$2>', | |
) | |
// Collapse tags with equal props but differing or no children like: | |
// <View …="…"> | |
// <View /> | |
// </View>`. | |
// or: | |
// <View …="…"/> | |
.replace(/(\s*)<(\w+)\n\1 {2}…="…"\n\1(\/?)>/g, '$1<$2 …="…"$3>'); | |
return prettyFormattedAndCollapsed; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment