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

Improve support for Enzyme's shallow rendering #1672

Merged
merged 19 commits into from
Jan 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions docs/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,20 @@ npm install --save-dev jest-emotion

Add the snapshot serializer in your [`setupTestFrameworkScriptFile`](http://facebook.github.io/jest/docs/en/configuration.html#setuptestframeworkscriptfile-string) _or_ at the top of your test file.

When using Enzyme, it's recommended to use the combined enzyme and emotion serializer to support shallow rendering:

```javascript
import { createSerializer } from 'jest-emotion/enzyme' // also adds the enzyme-to-json serializer

expect.addSnapshotSerializer(createSerializer())
```

Otherwise, use standard `createSerializer` method.

```javascript
import * as emotion from 'emotion'
import { createSerializer } from 'jest-emotion'

expect.addSnapshotSerializer(createSerializer(emotion))
expect.addSnapshotSerializer(createSerializer())
```

### Writing a test
Expand Down
1 change: 0 additions & 1 deletion packages/create-emotion/test/css.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// @flow
import 'test-utils/legacy-env'
import 'test-utils/next-env'
import React from 'react'
import renderer from 'react-test-renderer'
Expand Down
16 changes: 13 additions & 3 deletions packages/jest-emotion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,19 @@ The easiest way to test React components with emotion is with the snapshot seria
// jest.config.js
module.exports = {
// ... other config
snapshotSerializers: [
'jest-emotion' /* if needed other snapshotSerializers should go here */
]
snapshotSerializers: ['jest-emotion']
}
```

To assist with shallow rendering, there's a custom enzyme snapshot serializer, that includes the `enzyme-to-json`
serializer, which is available by importing `jest-emotion/enzyme`. If you already have the `enzyme-to-json` serializer
added to `snapshotSerializers`, it will need to be removed to allow this to work.

```js
// jest.config.js
module.exports = {
// ... other config
snapshotSerializers: ['jest-emotion/enzyme']
}
```

Expand Down
1 change: 1 addition & 0 deletions packages/jest-emotion/enzyme/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../src/enzyme'
8 changes: 8 additions & 0 deletions packages/jest-emotion/enzyme/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"main": "dist/jest-emotion.cjs.js",
"module": "dist/jest-emotion.esm.js",
"types": "../types/enzyme",
"preconstruct": {
"source": "../src/enzyme"
}
}
13 changes: 13 additions & 0 deletions packages/jest-emotion/enzyme/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// <reference types="jest" />

export interface CreateSerializerOptions {
classNameReplacer?: (className: string, index: number) => string
DOMElements?: boolean
}
export function createSerializer(
options?: CreateSerializerOptions
): jest.SnapshotSerializerPlugin
export const print: jest.SnapshotSerializerPlugin['print']
export const test: jest.SnapshotSerializerPlugin['test']
declare const serializer: jest.SnapshotSerializerPlugin
export default serializer
17 changes: 15 additions & 2 deletions packages/jest-emotion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
"version": "11.0.0-next.7",
"description": "Jest utilities for emotion",
"main": "dist/jest-emotion.cjs.js",
"module": "dist/jest-emotion.esm.js",
"types": "types/index.d.ts",
"files": [
"src",
"dist",
"types/*.d.ts",
"enzyme",
"serializer.js"
],
"scripts": {
Expand All @@ -20,6 +22,14 @@
"css": "^2.2.1",
"specificity": "^0.4.1"
},
"peerDependencies": {
Andarist marked this conversation as resolved.
Show resolved Hide resolved
"enzyme-to-json": "^3.2.1"
},
"peerDependenciesMeta": {
"enzyme-to-json": {
"optional": true
}
},
"devDependencies": {
"@emotion/core": "^11.0.0-next.7",
"dtslint": "^0.3.0",
Expand Down Expand Up @@ -47,7 +57,10 @@
"bugs": {
"url": "https://github.com/emotion-js/emotion/issues"
},
"browser": {
"./dist/jest-emotion.cjs.js": "./dist/jest-emotion.browser.cjs.js"
"preconstruct": {
"entrypoints": [
".",
"enzyme"
]
}
}
43 changes: 43 additions & 0 deletions packages/jest-emotion/src/enzyme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @flow
import type { Options } from './serializer'
import { createSerializer as createEmotionSerializer } from './serializer'
import { createSerializer as createEnzymeSerializer } from 'enzyme-to-json'

const enzymeSerializer = createEnzymeSerializer({})

const tickle = (wrapper: *) => {
if (typeof wrapper.dive === 'function') {
wrapper.find('EmotionCssPropInternal').forEach(el => el.dive())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we have a serializer that calls this and then calls out to the printer so we don't have to call enzyme-to-json ourselves?(We'd also want a WeakSet to store which wrappers we've done the printing on so we don't do it infinitely)

Copy link
Contributor Author

@ajs139 ajs139 Dec 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is order, we want to execute this on the enzyme objects, but our serializer works on the JSON objects. The other options would either to be to have two serializers, such as an init-emotion-enzyme serializer as well as the standard jest-emotion one, so the user could setup the correct order of ['init-emotion-enzyme', 'enzyme-to-json', 'jest-emotion'], or to export an initializer so that the get snapshot call would look something like expect(initializer(component)).toMatchSnapshot(). Both of these seemed awkward DX, this alongside the fact that we really do rely on enzyme-to-json led to the idea of simply tying them together here. It turns out that this makes it easier to setup, too, as neglecting to include both serializers would cause an error the way it was setup before. Open to other ideas, but this seemed to offer the right balance of DX and functionality.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, SGTM

}
return wrapper
}

export function createSerializer({
classNameReplacer,
DOMElements = true
}: Options = {}) {
const emotionSerializer = createEmotionSerializer({
classNameReplacer,
DOMElements
})
return {
test(node: *) {
return enzymeSerializer.test(node) || emotionSerializer.test(node)
},
print(node: *, printer: *) {
let result = node
if (enzymeSerializer.test(node)) {
const tickled = tickle(node)
result = enzymeSerializer.print(tickled, printer)
}
if (emotionSerializer.test(node)) {
result = emotionSerializer.print(result, printer)
}
return result
}
}
}

export const { print, test } = createSerializer()

export default { print, test }
117 changes: 2 additions & 115 deletions packages/jest-emotion/src/index.js
Original file line number Diff line number Diff line change
@@ -1,119 +1,6 @@
// @flow
import * as css from 'css'
import { replaceClassNames } from './replace-class-names'
import {
getClassNamesFromNodes,
isReactElement,
isEmotionCssPropElementType,
isEmotionCssPropEnzymeElement,
isDOMElement,
getStylesFromClassNames,
getStyleElements,
getKeys
} from './utils'

import { createSerializer, print, test } from './serializer'
export { matchers } from './matchers'

function getNodes(node, nodes = []) {
if (Array.isArray(node)) {
for (let child of node) {
getNodes(child, nodes)
}
return nodes
}

if (node.children) {
for (let child of node.children) {
getNodes(child, nodes)
}
}

if (typeof node === 'object') {
nodes.push(node)
}

return nodes
}

function getPrettyStylesFromClassNames(
classNames: Array<string>,
elements: Array<HTMLStyleElement>
) {
let styles = getStylesFromClassNames(classNames, elements)

let prettyStyles
try {
prettyStyles = css.stringify(css.parse(styles))
} catch (e) {
console.error(e)
throw new Error(`There was an error parsing the following css: "${styles}"`)
}
return prettyStyles
}

type Options = {
classNameReplacer?: (className: string, index: number) => string,
DOMElements?: boolean
}

function filterEmotionProps(props = {}) {
const {
css,
__EMOTION_TYPE_PLEASE_DO_NOT_USE__,
__EMOTION_LABEL_PLEASE_DO_NOT_USE__,
...rest
} = props

rest.css = 'unknown styles'

return rest
}

export function createSerializer({
classNameReplacer,
DOMElements = true
}: Options = {}) {
let cache = new WeakSet()
function print(val: *, printer: Function) {
if (isEmotionCssPropEnzymeElement(val)) {
return val.children.map(printer).join('\n')
}
if (isEmotionCssPropElementType(val)) {
return printer({
...val,
props: filterEmotionProps(val.props),
type: val.props.__EMOTION_TYPE_PLEASE_DO_NOT_USE__
})
}
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,
printedVal,
keys,
classNameReplacer
)
}

function test(val: *) {
return (
val &&
((!cache.has(val) &&
(isReactElement(val) || (DOMElements && isDOMElement(val)))) ||
isEmotionCssPropEnzymeElement(val) ||
isEmotionCssPropElementType(val))
)
}
return { test, print }
}

export let { print, test } = createSerializer()

export { createSerializer, print, test }
export default { print, test }
Loading