diff --git a/.flowconfig b/.flowconfig index ed3d80318..3d03e8e0e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -21,3 +21,4 @@ module.name_mapper='^\(babel-plugin-emotion\)$' -> '/packages/\1/s module.name_mapper='^\(emotion-theming\)$' -> '/packages/\1/src' module.name_mapper='^\(emotion-server\)$' -> '/packages/\1/src' module.name_mapper='^\(create-emotion-server\)$' -> '/packages/\1/src' +module.name_mapper='^\(jest-emotion\)$' -> '/packages/\1/src' diff --git a/docs/testing.md b/docs/testing.md index 488149b77..c5112f46c 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -4,7 +4,7 @@ Adding [snapshot tests with Jest](https://facebook.github.io/jest/docs/en/snapsh By diffing the serialized value of your React tree Jest can show you what changed in your app and allow you to fix it or update the snapshot. -By default snapshots with emotion show generated class names. Adding [jest-glamor-react](https://github.com/kentcdodds/jest-glamor-react) allows you to output the actual styles being applied. +By default snapshots with emotion show generated class names. Adding [jest-emotion](https://github.com/emotion-js/emotion/tree/master/packages/jest-emotion) allows you to output the actual styles being applied. @@ -12,16 +12,16 @@ By default snapshots with emotion show generated class names. Adding [jest-glamo ### Installation ```bash -npm install --save-dev jest-glamor-react +npm install --save-dev jest-emotion ``` **testSetup.js** _or_ at the top of your test file ```javascript -import { sheet } from 'emotion' -import serializer from 'jest-glamor-react' +import * as emotion from 'emotion' +import { createSerializer } from 'jest-emotion' -expect.addSnapshotSerializer(serializer(sheet)) +expect.addSnapshotSerializer(createSerializer(emotion)) ``` **package.json** @@ -49,7 +49,6 @@ test('Link renders correctly', () => { ``` ### Notes -It's recommended to set your Jest `testEnvironment` to `jsdom`, but you can mock global browser objects instead. -Your snapshot class names will now appear as `glamor-[0...n]` +Your snapshot class names will appear as `emotion-[0...n]` instead of `css-[hash]`. diff --git a/jest.dist.json b/jest.dist.json index 064bdd38f..498830b70 100644 --- a/jest.dist.json +++ b/jest.dist.json @@ -11,6 +11,7 @@ "^emotion-theming$": "/packages/emotion-theming/dist/index.cjs.js", "^babel-plugin-emotion": "/packages/babel-plugin-emotion/lib", "^create-emotion$": "/packages/create-emotion/dist/index.cjs.js", + "^jest-emotion$": "/packages/jest-emotion/lib", "^create-emotion-styled$": "/packages/create-emotion-styled/dist/index.cjs.js", "^create-emotion-server$": "/packages/create-emotion-server/lib" }, diff --git a/package.json b/package.json index a663995a0..a4822a6fc 100644 --- a/package.json +++ b/package.json @@ -159,6 +159,7 @@ "^emotion-theming$": "/packages/emotion-theming/src", "^babel-plugin-emotion": "/packages/babel-plugin-emotion/src", "^create-emotion$": "/packages/create-emotion/src", + "^jest-emotion$": "/packages/jest-emotion/src", "^create-emotion-styled$": "/packages/create-emotion-styled/src", "^create-emotion-server$": "/packages/create-emotion-server/src" }, diff --git a/packages/babel-plugin-emotion/test/macro/__snapshots__/css.test.js.snap b/packages/babel-plugin-emotion/test/macro/__snapshots__/css.test.js.snap index 587f668a3..e3b311255 100644 --- a/packages/babel-plugin-emotion/test/macro/__snapshots__/css.test.js.snap +++ b/packages/babel-plugin-emotion/test/macro/__snapshots__/css.test.js.snap @@ -157,7 +157,7 @@ exports[`css flushes correctly 1`] = ` exports[`css flushes correctly 2`] = `
`; @@ -300,7 +300,7 @@ exports[`css nested selector without parent declaration 1`] = ` exports[`css null rule 1`] = `
`; diff --git a/packages/babel-plugin-emotion/test/macro/__snapshots__/react.test.js.snap b/packages/babel-plugin-emotion/test/macro/__snapshots__/react.test.js.snap index c7866c5e0..fafb35519 100644 --- a/packages/babel-plugin-emotion/test/macro/__snapshots__/react.test.js.snap +++ b/packages/babel-plugin-emotion/test/macro/__snapshots__/react.test.js.snap @@ -716,7 +716,7 @@ exports[`styled theming 1`] = ` >
this will be green then pink
@@ -743,7 +743,7 @@ exports[`styled theming 2`] = ` >
this will be green then pink
diff --git a/packages/create-emotion/test/__snapshots__/css.test.js.snap b/packages/create-emotion/test/__snapshots__/css.test.js.snap index 917b12e0f..f10fb7fda 100644 --- a/packages/create-emotion/test/__snapshots__/css.test.js.snap +++ b/packages/create-emotion/test/__snapshots__/css.test.js.snap @@ -136,8 +136,12 @@ exports[`css css variables 1`] = ` `; exports[`css explicit & 1`] = ` +.glamor-0.another-class { + display: flex; +} +
`; @@ -149,7 +153,7 @@ exports[`css explicit & 2`] = ` exports[`css explicit false 1`] = `
`; @@ -195,7 +199,7 @@ exports[`css flushes correctly 1`] = ` exports[`css flushes correctly 2`] = `
`; @@ -367,19 +371,19 @@ exports[`css nested selector without parent declaration 1`] = ` exports[`css null rule 1`] = `
`; exports[`css null value 1`] = `
`; exports[`css null value 2`] = `
`; diff --git a/packages/create-emotion/test/__snapshots__/styled.test.js.snap b/packages/create-emotion/test/__snapshots__/styled.test.js.snap index 31cbbd87e..5c1486cb1 100644 --- a/packages/create-emotion/test/__snapshots__/styled.test.js.snap +++ b/packages/create-emotion/test/__snapshots__/styled.test.js.snap @@ -694,7 +694,7 @@ exports[`styled theming 1`] = ` >
this will be green then pink
@@ -721,7 +721,7 @@ exports[`styled theming 2`] = ` >
this will be green then pink
@@ -766,11 +766,11 @@ exports[`styled with higher order component that hoists statics 1`] = ` /> `; -exports[`styled withComponent creates a new, unique stable class per invocation 1`] = `"css-10q4z3a46"`; +exports[`styled withComponent creates a new, unique stable class per invocation 1`] = `"css-1mkg15o46"`; -exports[`styled withComponent creates a new, unique stable class per invocation 2`] = `"css-10q4z3a47"`; +exports[`styled withComponent creates a new, unique stable class per invocation 2`] = `"css-1mkg15o47"`; -exports[`styled withComponent creates a new, unique stable class per invocation 3`] = `"css-10q4z3a48"`; +exports[`styled withComponent creates a new, unique stable class per invocation 3`] = `"css-1mkg15o48"`; exports[`styled withComponent will replace tags but keep styling classes 1`] = ` .glamor-0 { diff --git a/packages/create-emotion/test/css.test.js b/packages/create-emotion/test/css.test.js index d049a7cfc..ea92f8567 100644 --- a/packages/create-emotion/test/css.test.js +++ b/packages/create-emotion/test/css.test.js @@ -1,10 +1,13 @@ // @flow import React from 'react' import renderer from 'react-test-renderer' -import serializer from 'jest-glamor-react' +import { createSerializer } from 'jest-emotion' +// eslint-disable-next-line import/no-duplicates import { css as differentCss, flush, sheet } from './emotion-instance' +// eslint-disable-next-line import/no-duplicates +import * as emotion from './emotion-instance' -expect.addSnapshotSerializer(serializer(sheet)) +expect.addSnapshotSerializer(createSerializer(emotion)) describe('css', () => { test('float property', () => { diff --git a/packages/create-emotion/test/styled.test.js b/packages/create-emotion/test/styled.test.js index 5e292db35..0e5fa059c 100644 --- a/packages/create-emotion/test/styled.test.js +++ b/packages/create-emotion/test/styled.test.js @@ -1,8 +1,11 @@ // @flow import React from 'react' import renderer from 'react-test-renderer' -import styled, { css, flush, sheet } from './emotion-instance' -import serializer from 'jest-glamor-react' +// eslint-disable-next-line import/no-duplicates +import styled, { css, flush } from './emotion-instance' +// eslint-disable-next-line import/no-duplicates +import * as emotion from './emotion-instance' +import { createSerializer } from 'jest-emotion' import { ThemeProvider } from 'emotion-theming' import hoistNonReactStatics from 'hoist-non-react-statics' import { TARGET_KEY } from 'emotion-utils' @@ -11,7 +14,7 @@ import enzymeToJson from 'enzyme-to-json' import { lighten, hiDPI, modularScale } from 'polished' -expect.addSnapshotSerializer(serializer(sheet)) +expect.addSnapshotSerializer(createSerializer(emotion)) describe('styled', () => { beforeEach(() => flush()) diff --git a/packages/emotion/test/__snapshots__/css.test.js.snap b/packages/emotion/test/__snapshots__/css.test.js.snap index 7437d736d..07c6f5167 100644 --- a/packages/emotion/test/__snapshots__/css.test.js.snap +++ b/packages/emotion/test/__snapshots__/css.test.js.snap @@ -152,8 +152,15 @@ exports[`css css variables 1`] = ` `; exports[`css explicit & 1`] = ` +.glamor-0.another-class { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} +
`; @@ -168,7 +175,7 @@ exports[`css explicit & 2`] = ` exports[`css explicit false 1`] = `
`; @@ -220,7 +227,7 @@ exports[`css flushes correctly 1`] = ` exports[`css flushes correctly 2`] = `
`; @@ -454,19 +461,19 @@ exports[`css nested selector without parent declaration 1`] = ` exports[`css null rule 1`] = `
`; exports[`css null value 1`] = `
`; exports[`css null value 2`] = `
`; @@ -527,6 +534,22 @@ exports[`css return function in interpolation 1`] = ` /> `; +exports[`css rule after media query 1`] = ` +@media (min-width:600px) { + .glamor-0 { + color: green; + } +} + +.glamor-0:hover { + color: hotpink; +} + +
+`; + exports[`css simple composition 1`] = ` .glamor-0 { display: -webkit-box; diff --git a/packages/emotion/test/css.test.js b/packages/emotion/test/css.test.js index 6c5cf5104..9144ca32a 100644 --- a/packages/emotion/test/css.test.js +++ b/packages/emotion/test/css.test.js @@ -388,4 +388,16 @@ describe('css', () => { const tree = renderer.create(
).toJSON() expect(tree).toMatchSnapshot() }) + test('rule after media query', () => { + const cls1 = css` + @media (min-width: 600px) { + color: green; + } + &:hover { + color: hotpink; + } + ` + const tree = renderer.create(
).toJSON() + expect(tree).toMatchSnapshot() + }) }) diff --git a/packages/emotion/test/extract/__snapshots__/extract.test.js.snap b/packages/emotion/test/extract/__snapshots__/extract.test.js.snap index 5c758af22..4ef54f45a 100644 --- a/packages/emotion/test/extract/__snapshots__/extract.test.js.snap +++ b/packages/emotion/test/extract/__snapshots__/extract.test.js.snap @@ -2,7 +2,7 @@ exports[`styled basic render nested 1`] = `

hello world

@@ -10,7 +10,7 @@ exports[`styled basic render nested 1`] = ` exports[`styled className prop on styled 1`] = `

hello world

@@ -18,7 +18,7 @@ exports[`styled className prop on styled 1`] = ` exports[`styled no dynamic 1`] = `

hello world

diff --git a/packages/emotion/test/no-babel/__snapshots__/index.test.js.snap b/packages/emotion/test/no-babel/__snapshots__/index.test.js.snap index 72c57309d..7ed4d001d 100644 --- a/packages/emotion/test/no-babel/__snapshots__/index.test.js.snap +++ b/packages/emotion/test/no-babel/__snapshots__/index.test.js.snap @@ -172,7 +172,7 @@ exports[`css no dynamic 1`] = ` exports[`css null rule 1`] = `
`; diff --git a/packages/jest-emotion/README.md b/packages/jest-emotion/README.md new file mode 100644 index 000000000..f32d5e4df --- /dev/null +++ b/packages/jest-emotion/README.md @@ -0,0 +1,80 @@ +# jest-emotion + +> Jest testing utilities for emotions + +# Installation + +```bash +npm install --save-dev jest-emotion +``` + +# Snapshot Serializer + +The easiest way to test React components with emotion is with the snapshot serializer. (the example below is with react-test-renderer but jest-emotion also works with enzyme) + +```jsx +import React from 'react' +import renderer from 'react-test-renderer' +import { createSerializer } from 'jest-emotion' +import * as emotion from 'emotion' +import styled from 'react-emotion' + +expect.addSnapshotSerializer(createSerializer(emotion)) + +test('renders with correct styles', () => { + const H1 = styled.h1` + float: left; + ` + + const tree = renderer.create(

hello world

).toJSON() + + expect(tree).toMatchSnapshot() +}) +``` + +Refer to the [testing doc](https://github.com/emotion-js/emotion/blob/master/docs/testing.md) for more information about snapshot testing with emotion. + +## Options + +# `classNameReplacer` + +jest-emotion's snapshot serializer replaces the hashes in class names with an index so that things like whitespace changes won't break snapshots. It optionally accepts a custom class name replacer, it defaults to the below. + +```jsx +function classNameReplacer(className, index) { + return `emotion-${index}` +} +``` +```jsx +import * as emotion from 'emotion' +import { createSerializer } from 'jest-emotion' + +expect.addSnapshotSerializer( + createSerializer(emotion, { + classNameReplacer(className, index) { + return `my-new-class-name-${index}` + } + }) +) +``` +# getStyles + +jest-emotion also allows you to get all the css that emotion has inserted. This is meant to be an escape hatch if you don't use React or you want to build your own utilities for testing with emotion. + +```jsx +import * as emotion from 'emotion' +import { css } from 'emotion' +import { getStyles } from 'jest-emotion' + +test('correct styles are inserted', () => { + const cls = css` + display: flex; + ` + + expect(getStyles(emotion)).toMatchSnapshot() +}) +``` + +## Thanks + +Thanks to [Kent C. Dodds](https://twitter.com/kentcdodds) who wrote [jest-glamor-react](https://github.com/kentcdodds/jest-glamor-react) which this library is largely based on. \ No newline at end of file diff --git a/packages/jest-emotion/package.json b/packages/jest-emotion/package.json new file mode 100644 index 000000000..a89184aeb --- /dev/null +++ b/packages/jest-emotion/package.json @@ -0,0 +1,40 @@ +{ + "name": "jest-emotion", + "version": "8.0.12", + "description": "Jest utilities for emotion", + "main": "lib/index.js", + "files": [ + "src", + "lib" + ], + "scripts": { + "build": "npm-run-all clean babel", + "babel": "babel src -d lib", + "watch": "babel src -d lib --watch", + "clean": "rimraf lib" + }, + "dependencies": { + "css": "^2.2.1" + }, + "devDependencies": { + "babel-cli": "^6.24.1", + "npm-run-all": "^4.0.2", + "rimraf": "^2.6.1" + }, + "author": "Kye Hohenberger", + "homepage": "https://emotion.sh", + "license": "MIT", + "repository": "https://github.com/emotion-js/emotion/tree/master/packages/jest-emotion-react", + "keywords": [ + "styles", + "emotion", + "react", + "css", + "css-in-js", + "jest", + "snapshot" + ], + "bugs": { + "url": "https://github.com/emotion-js/emotion/issues" + } +} diff --git a/packages/jest-emotion/src/index.js b/packages/jest-emotion/src/index.js new file mode 100644 index 000000000..3421451c6 --- /dev/null +++ b/packages/jest-emotion/src/index.js @@ -0,0 +1,132 @@ +// @flow +import * as css from 'css' +import { + replaceClassNames, + type ClassNameReplacer +} from './replace-class-names' +import type { Emotion } from 'create-emotion' + +type Options = { + classNameReplacer: ClassNameReplacer +} + +function getNodes(node, nodes = []) { + if (node.children) { + node.children.forEach(child => getNodes(child, nodes)) + } + + if (typeof node === 'object') { + nodes.push(node) + } + + return nodes +} + +function getSelectors(nodes) { + return nodes.reduce( + (selectors, node) => getSelectorsFromProps(selectors, node.props), + [] + ) +} + +function getSelectorsFromProps(selectors, props) { + const className = props.className || props.class + if (className) { + selectors = selectors.concat(className.split(' ').map(cn => `.${cn}`)) + } + return selectors +} + +function filterChildSelector(baseSelector) { + if (baseSelector.slice(-1) === '>') { + return baseSelector.slice(0, -1) + } + return baseSelector +} + +export function getStyles(emotion: Emotion) { + return Object.keys(emotion.caches.inserted).reduce((style, current) => { + if (emotion.caches.inserted[current] === true) { + return style + } + return style + emotion.caches.inserted[current] + }, '') +} + +function test(val: *) { + return ( + val && + !val.withEmotionStyles && + val.$$typeof === Symbol.for('react.test.json') + ) +} + +export function createSerializer( + emotion: Emotion, + { classNameReplacer }: Options = {} +) { + // in case we add a key option + const key = 'css' + + function print(val: *, printer: Function) { + const nodes = getNodes(val) + markNodes(nodes) + const selectors = getSelectors(nodes) + const styles = getStylesFromSelectors(selectors) + const printedVal = printer(val) + return replaceClassNames( + selectors, + styles, + printedVal, + key, + classNameReplacer + ) + } + + function markNodes(nodes) { + nodes.forEach(node => { + node.withEmotionStyles = true + }) + } + + function getStylesFromSelectors(nodeSelectors) { + const styles = getStyles(emotion) + let ast + try { + ast = css.parse(styles) + } catch (e) { + console.error(e) + throw new Error( + `There was an error parsing css in jest-emotion-react: "${styles}"` + ) + } + ast.stylesheet.rules = ast.stylesheet.rules.reduce(reduceRules, []) + + const ret = css.stringify(ast) + return ret + + function reduceRules(rules, rule) { + let shouldIncludeRule = false + if (rule.type === 'rule') { + shouldIncludeRule = rule.selectors.some(selector => { + const baseSelector = filterChildSelector( + selector.split(/:| |\./).filter(s => !!s)[0] + ) + return nodeSelectors.some( + sel => sel === baseSelector || sel === `.${baseSelector}` + ) + }) + } + if (rule.type === 'media' || rule.type === 'supports') { + rule.rules = rule.rules.reduce(reduceRules, []) + + if (rule.rules.length) { + shouldIncludeRule = true + } + } + return shouldIncludeRule ? rules.concat(rule) : rules + } + } + + return { test, print } +} diff --git a/packages/jest-emotion/src/replace-class-names.js b/packages/jest-emotion/src/replace-class-names.js new file mode 100644 index 000000000..185c1aa5d --- /dev/null +++ b/packages/jest-emotion/src/replace-class-names.js @@ -0,0 +1,27 @@ +// @flow +function defaultClassNameReplacer(className, index) { + // Change this to emotion before merging + return `glamor-${index}` +} + +export type ClassNameReplacer = (className: string, index: number) => string + +export const replaceClassNames = ( + selectors: Array, + styles: string, + code: string, + key: string, + replacer: ClassNameReplacer = defaultClassNameReplacer +) => { + let index = 0 + return selectors.reduce((acc, className) => { + if (className.indexOf(`.${key}-`) === 0) { + const escapedRegex = new RegExp( + className.replace('.', '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'), + 'g' + ) + return acc.replace(escapedRegex, replacer(className, index++)) + } + return acc + }, `${styles}${styles ? '\n\n' : ''}${code}`) +} diff --git a/packages/react-emotion/test/__snapshots__/index.test.js.snap b/packages/react-emotion/test/__snapshots__/index.test.js.snap index 7e134ef2e..dc1251801 100644 --- a/packages/react-emotion/test/__snapshots__/index.test.js.snap +++ b/packages/react-emotion/test/__snapshots__/index.test.js.snap @@ -674,6 +674,18 @@ exports[`styled theme prop exists without ThemeProvider with a theme prop on the /> `; +exports[`styled theme with react-test-renderer 1`] = ` +.glamor-0 { + color: pink; +} + +
+ this will be pink +
+`; + exports[`styled themes 1`] = ` .glamor-0 { background-color: #ffd43b; @@ -709,7 +721,7 @@ exports[`styled theming 1`] = ` >
this will be green then pink
@@ -736,7 +748,7 @@ exports[`styled theming 2`] = ` >
this will be green then pink
@@ -784,11 +796,11 @@ exports[`styled with higher order component that hoists statics 1`] = ` /> `; -exports[`styled withComponent creates a new, unique stable class per invocation 1`] = `"css-1rh8huw46"`; +exports[`styled withComponent creates a new, unique stable class per invocation 1`] = `"css-1amggv547"`; -exports[`styled withComponent creates a new, unique stable class per invocation 2`] = `"css-1rh8huw47"`; +exports[`styled withComponent creates a new, unique stable class per invocation 2`] = `"css-1amggv548"`; -exports[`styled withComponent creates a new, unique stable class per invocation 3`] = `"css-1rh8huw48"`; +exports[`styled withComponent creates a new, unique stable class per invocation 3`] = `"css-1amggv549"`; exports[`styled withComponent will replace tags but keep styling classes 1`] = ` .glamor-0 { diff --git a/packages/react-emotion/test/index.test.js b/packages/react-emotion/test/index.test.js index c02e48d98..b9b2794e5 100644 --- a/packages/react-emotion/test/index.test.js +++ b/packages/react-emotion/test/index.test.js @@ -578,6 +578,21 @@ describe('styled', () => { expect(tree).toMatchSnapshot() }) + test('theme with react-test-renderer', () => { + const Div = styled.div` + color: ${props => props.theme.primary}; + ` + const tree = renderer + .create( + + {
this will be pink
} +
+ ) + .toJSON() + + expect(tree).toMatchSnapshot() + }) + test('change theme', () => { const Div = styled.div` color: ${props => props.theme.primary}; diff --git a/test/testSetup.js b/test/testSetup.js index 72366ec80..32afbd278 100644 --- a/test/testSetup.js +++ b/test/testSetup.js @@ -1,12 +1,12 @@ // @flow /* eslint-env jest */ -import serializer from 'jest-glamor-react' -import { sheet } from 'emotion' +import { createSerializer } from 'jest-emotion' +import * as emotion from 'emotion' import Enzyme from 'enzyme' import Adapter from 'enzyme-adapter-react-16' import prettyCSS from './pretty-css' -expect.addSnapshotSerializer(serializer(sheet)) +expect.addSnapshotSerializer(createSerializer(emotion)) expect.addSnapshotSerializer(prettyCSS)