diff --git a/packages/jest-emotion/CHANGELOG.md b/packages/jest-emotion/CHANGELOG.md index 87ed36bd0..52ee986fd 100644 --- a/packages/jest-emotion/CHANGELOG.md +++ b/packages/jest-emotion/CHANGELOG.md @@ -47,6 +47,12 @@ - @emotion/core@11.0.0-next.0 - emotion@11.0.0-next.0 +## 10.0.25 + +### Patch Changes + +- [`858c6e70`](https://github.com/emotion-js/emotion/commit/858c6e70e2aa83d159dba00af16f1e34a6d93fd0) [#1648](https://github.com/emotion-js/emotion/pull/1648) Thanks [@ajs139](https://github.com/ajs139)! - Improve support for Enzyme's shallow rendering. + ## 10.0.17 ### Patch Changes diff --git a/packages/jest-emotion/src/index.js b/packages/jest-emotion/src/index.js index cbfaefa98..13b739bf1 100644 --- a/packages/jest-emotion/src/index.js +++ b/packages/jest-emotion/src/index.js @@ -9,7 +9,8 @@ import { isDOMElement, getStylesFromClassNames, getStyleElements, - getKeys + getKeys, + flatMap } from './utils' export { matchers } from './matchers' @@ -69,14 +70,50 @@ function filterEmotionProps(props = {}) { return rest } +function hasIntersection(left: any[], right: any[]) { + return left.some(value => right.includes(value)) +} + +function isShallowEnzymeElement(element, classNames) { + const delimiter = ' ' + let childClassNames = flatMap(element.children || [], ({ props = {} }) => + (props.className || '').split(delimiter) + ).filter(Boolean) + + return !hasIntersection(classNames, childClassNames) +} + export function createSerializer({ classNameReplacer, DOMElements = true }: Options = {}) { let cache = new WeakSet() function print(val: *, printer: Function) { + let elements = getStyleElements() + let keys = getKeys(elements) if (isEmotionCssPropEnzymeElement(val)) { - return val.children.map(printer).join('\n') + let cssClassNames = (val.props.css.name || '').split(' ') + let expectedClassNames = flatMap(cssClassNames, cssClassName => + keys.map(key => `${key}-${cssClassName}`) + ) + // if this is a shallow element, we need to manufacture the className + // since the underlying component is not rendered. + if (isShallowEnzymeElement(val, expectedClassNames)) { + let className = [val.props.className] + .concat(expectedClassNames) + .filter(Boolean) + .join(' ') + return printer({ + ...val, + props: filterEmotionProps({ + ...val.props, + className + }), + type: val.props.__EMOTION_TYPE_PLEASE_DO_NOT_USE__ + }) + } else { + return val.children.map(printer).join('\n') + } } if (isEmotionCssPropElementType(val)) { return printer({ @@ -87,12 +124,10 @@ export function createSerializer({ } const nodes = getNodes(val) const classNames = getClassNamesFromNodes(nodes) - let elements = getStyleElements() const styles = getPrettyStylesFromClassNames(classNames, elements) nodes.forEach(cache.add, cache) const printedVal = printer(val) nodes.forEach(cache.delete, cache) - let keys = getKeys(elements) return replaceClassNames( classNames, styles, diff --git a/packages/jest-emotion/src/utils.js b/packages/jest-emotion/src/utils.js index a40e34e25..326f65c2e 100644 --- a/packages/jest-emotion/src/utils.js +++ b/packages/jest-emotion/src/utils.js @@ -4,7 +4,7 @@ function last(arr) { return arr.length > 0 ? arr[arr.length - 1] : undefined } -function flatMap(arr, iteratee) { +export function flatMap(arr: T[], iteratee: (arg: T) => S[] | S): S[] { return [].concat(...arr.map(iteratee)) } @@ -37,14 +37,21 @@ function isTagWithClassName(node) { return node.prop('className') && typeof node.type() === 'string' } -function getClassNamesFromEnzyme(selectors, node) { - // We need to dive if we have selected a styled child from a shallow render - const actualComponent = shouldDive(node) ? node.dive() : node +function findNodeWithClassName(node) { // Find the first node with a className prop - const components = actualComponent.findWhere(isTagWithClassName) - const classes = components.length && components.first().prop('className') + const found = node.findWhere(isTagWithClassName) + return found.length ? found.first() : null +} - return getClassNames(selectors, classes) +function getClassNameProp(node) { + return (node && node.prop('className')) || '' +} + +function getClassNamesFromEnzyme(selectors, node) { + // We need to dive in to get the className if we have a styled element from a shallow render + let isShallow = shouldDive(node) + let nodeWithClassName = findNodeWithClassName(isShallow ? node.dive() : node) + return getClassNames(selectors, getClassNameProp(nodeWithClassName)) } function getClassNamesFromCheerio(selectors, node) { @@ -152,7 +159,7 @@ export function getStylesFromClassNames( let keyframes = {} let styles = '' - flatMap(elements, getElementRules).forEach(rule => { + flatMap(elements, getElementRules).forEach((rule: string) => { if (selectorPattern.test(rule)) { styles += rule } diff --git a/packages/jest-emotion/test/__snapshots__/react-enzyme.test.js.snap b/packages/jest-emotion/test/__snapshots__/react-enzyme.test.js.snap index bedfd8a7c..11522fb4e 100644 --- a/packages/jest-emotion/test/__snapshots__/react-enzyme.test.js.snap +++ b/packages/jest-emotion/test/__snapshots__/react-enzyme.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`enzyme mount test 1`] = ` +exports[`enzyme mount basic 1`] = ` .emotion-0 { background-color: red; } @@ -18,7 +18,7 @@ exports[`enzyme mount test 1`] = ` `; -exports[`enzyme test with prop containing css element 1`] = ` +exports[`enzyme mount with prop containing css element 1`] = ` .emotion-0 { background-color: blue; } @@ -48,28 +48,7 @@ exports[`enzyme test with prop containing css element 1`] = ` `; -exports[`enzyme test with prop containing css element in fragment 1`] = ` -.emotion-0 { - background-color: blue; -} - -.emotion-0 { - background-color: blue; -} - -
- Array [ - "x", -
- y -
, - ] -
-`; - -exports[`enzyme test with prop containing css element not at the top level 1`] = ` +exports[`enzyme mount with prop containing css element not at the top level 1`] = ` .emotion-0 { background-color: blue; } @@ -103,7 +82,7 @@ exports[`enzyme test with prop containing css element not at the top level 1`] = `; -exports[`enzyme test with prop containing css element with other label 1`] = ` +exports[`enzyme mount with prop containing css element with other label 1`] = ` .emotion-0 { background-color: blue; } @@ -141,7 +120,7 @@ exports[`enzyme test with prop containing css element with other label 1`] = ` `; -exports[`enzyme test with prop containing css element with other props 1`] = ` +exports[`enzyme mount with prop containing css element with other props 1`] = ` .emotion-0 { background-color: blue; } @@ -172,3 +151,97 @@ exports[`enzyme test with prop containing css element with other props 1`] = ` `; + +exports[`enzyme shallow basic 1`] = ` +
+ hello +
+`; + +exports[`enzyme shallow with prop containing css element 1`] = ` +
+

+ Hello +

+ + World! +
+`; + +exports[`enzyme shallow with prop containing css element not at the top level 1`] = ` +
+ + Hello +

+ } + > + World! +
+
+`; + +exports[`enzyme shallow with prop containing css element with other label 1`] = ` + + } +> +

+ Hello +

+ + World! +
+`; + +exports[`enzyme shallow with prop containing css element with other props 1`] = ` +
+

+ Hello +

+ + World! +
+`; + +exports[`enzyme with prop containing css element in fragment 1`] = ` +.emotion-0 { + background-color: blue; +} + +.emotion-0 { + background-color: blue; +} + +
+ Array [ + "x", +
+ y +
, + ] +
+`; diff --git a/packages/jest-emotion/test/matchers.test.js b/packages/jest-emotion/test/matchers.test.js index 67f093e4f..dee09aa44 100644 --- a/packages/jest-emotion/test/matchers.test.js +++ b/packages/jest-emotion/test/matchers.test.js @@ -19,8 +19,6 @@ describe('toHaveStyleRule', () => { width: 100%; ` - const enzymeMethods = ['mount', 'render'] - it('matches styles on the top-most node passed in', () => { const tree = renderer .create( @@ -56,21 +54,49 @@ describe('toHaveStyleRule', () => { expect(svgNode).toHaveStyleRule('width', expect.stringMatching(/.*%$/)) }) - it('supports enzyme render methods', () => { + it('supports enzyme `mount` method', () => { const Component = () => (
) - enzymeMethods.forEach(method => { - const wrapper = enzyme[method]() - expect(wrapper).toHaveStyleRule('color', 'red') - expect(wrapper).not.toHaveStyleRule('width', '100%') - const svgNode = wrapper.find('svg') - expect(svgNode).toHaveStyleRule('width', '100%') - expect(svgNode).not.toHaveStyleRule('color', 'red') - }) + const wrapper = enzyme.mount() + expect(wrapper).toHaveStyleRule('color', 'red') + expect(wrapper).not.toHaveStyleRule('width', '100%') + const svgNode = wrapper.find('svg') + expect(svgNode).toHaveStyleRule('width', '100%') + expect(svgNode).not.toHaveStyleRule('color', 'red') + }) + + it('supports enzyme `render` method', () => { + const Component = () => ( +
+ +
+ ) + + const wrapper = enzyme.render() + expect(wrapper).toHaveStyleRule('color', 'red') + expect(wrapper).not.toHaveStyleRule('width', '100%') + const svgNode = wrapper.find('svg') + expect(svgNode).toHaveStyleRule('width', '100%') + expect(svgNode).not.toHaveStyleRule('color', 'red') + }) + + it('supports enzyme `shallow` method', () => { + const Component = () => ( +
+ +
+ ) + + const wrapper = enzyme.shallow() + expect(wrapper).toHaveStyleRule('color', 'red') + expect(wrapper).not.toHaveStyleRule('width', '100%') + const svgNode = wrapper.childAt(0) + expect(svgNode).toHaveStyleRule('width', '100%') + expect(svgNode).not.toHaveStyleRule('color', 'red') }) // i think this isn't working because of forwardRef @@ -81,8 +107,7 @@ describe('toHaveStyleRule', () => { const Svg = styled('svg')` width: 100%; ` - - enzymeMethods.forEach(method => { + ;['mount', 'render', 'shallow'].forEach(method => { const wrapper = enzyme[method](
diff --git a/packages/jest-emotion/test/react-enzyme.test.js b/packages/jest-emotion/test/react-enzyme.test.js index 07514554a..e623865cc 100644 --- a/packages/jest-emotion/test/react-enzyme.test.js +++ b/packages/jest-emotion/test/react-enzyme.test.js @@ -1,130 +1,139 @@ import 'test-utils/legacy-env' /** @jsx jsx */ +import jestInCase from 'jest-in-case' import * as enzyme from 'enzyme' import { jsx } from '@emotion/core' -import { createSerializer as createEnzymeSerializer } from 'enzyme-to-json' import { createSerializer } from 'jest-emotion' -import { toMatchSnapshot } from 'jest-snapshot' import React from 'react' - -const createEnzymeSnapshotMatcher = serializerOptions => { - const serializer = createEnzymeSerializer(serializerOptions) - const identityPrinter = v => v - - return function(val) { - return toMatchSnapshot.call(this, serializer.print(val, identityPrinter)) - } -} +import toJson from 'enzyme-to-json' expect.addSnapshotSerializer(createSerializer()) -expect.extend({ - toMatchShallowSnapshot: createEnzymeSnapshotMatcher(), - toMatchDeepSnapshot: createEnzymeSnapshotMatcher({ mode: 'deep' }) -}) - -test('enzyme mount test', () => { - const Greeting = ({ children }) => ( -
{children}
- ) - const tree = enzyme.mount(hello) - expect(tree).toMatchShallowSnapshot() -}) - -test('enzyme test with prop containing css element', () => { - const Greeting = ({ children, content }) => ( -
- {content} {children} -
- ) - - const tree = enzyme.mount( - Hello

}> - World! -
- ) - expect(tree).toMatchShallowSnapshot() -}) - -test('enzyme test with prop containing css element not at the top level', () => { - const Greeting = ({ children, content }) => ( -
- {content} {children} -
- ) +const cases = { + basic: { + render() { + const Greeting = ({ children }) => ( +
{children}
+ ) + return hello + } + }, + 'with prop containing css element': { + render() { + const Greeting = ({ children, content }) => ( +
+ {content} {children} +
+ ) + return ( + Hello

}> + World! +
+ ) + } + }, + 'with prop containing css element not at the top level': { + render() { + const Greeting = ({ children, content }) => ( +
+ {content} {children} +
+ ) - const tree = enzyme.mount( -
- - Hello -

- } - > - World! -
-
- ) - expect(tree).toMatchShallowSnapshot() -}) + return ( +
+ + Hello +

+ } + > + World! +
+
+ ) + } + }, + 'with prop containing css element with other props': { + render() { + const Greeting = ({ children, content }) => ( +
+ {content} {children} +
+ ) -test('enzyme test with prop containing css element with other props', () => { - const Greeting = ({ children, content }) => ( -
- {content} {children} -
- ) - - const tree = enzyme.mount( - - Hello -

+ return ( + + Hello +

+ } + > + World! +
+ ) + } + }, + 'with prop containing css element with other label': { + render() { + const Thing = ({ content, children }) => { + return children } - > - World! -
- ) - expect(tree).toMatchShallowSnapshot() -}) + const Greeting = ({ children, content }) => ( + }> + {content} {children} + + ) -test('enzyme test with prop containing css element with other label', () => { - const Thing = ({ content, children }) => { - return children + return ( + + Hello +

+ } + > + World! +
+ ) + } } - const Greeting = ({ children, content }) => ( - }> - {content} {children} - - ) +} - const tree = enzyme.mount( - - Hello -

- } - > - World! -
+describe('enzyme', () => { + jestInCase( + 'shallow', + ({ render }) => { + const wrapper = enzyme.shallow(render()) + expect(toJson(wrapper)).toMatchSnapshot() + }, + cases ) - expect(tree).toMatchShallowSnapshot() -}) -test('enzyme test with prop containing css element in fragment', () => { - const FragmentComponent = () => ( - - x
y
-
+ jestInCase( + 'mount', + ({ render }) => { + const wrapper = enzyme.mount(render()) + expect(toJson(wrapper)).toMatchSnapshot() + }, + cases ) - const tree = enzyme.mount( -
- -
- ) - expect(tree).toMatchDeepSnapshot() + test('with prop containing css element in fragment', () => { + const FragmentComponent = () => ( + + x
y
+
+ ) + + const wrapper = enzyme.mount( +
+ +
+ ) + + expect(toJson(wrapper, { mode: 'deep' })).toMatchSnapshot() + }) })