diff --git a/README/react-css-modules.png b/.README/react-css-modules.png similarity index 100% rename from README/react-css-modules.png rename to .README/react-css-modules.png diff --git a/README/react-css-modules.sketch b/.README/react-css-modules.sketch similarity index 100% rename from README/react-css-modules.sketch rename to .README/react-css-modules.sketch diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..3751ba8 --- /dev/null +++ b/.babelrc @@ -0,0 +1,19 @@ +{ + "plugins": [ + "add-module-exports", + "lodash", + "transform-class-properties", + [ + "transform-es2015-classes", + { + "loose": true + } + ], + "transform-proto-to-assign" + ], + "presets": [ + "es2015", + "stage-0", + "react" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0f17867 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..bcaad1b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": [ + "canonical", + "canonical/mocha" + ] +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2f093a7 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: gajus +patreon: gajus diff --git a/.gitignore b/.gitignore index 14e7b61..df978de 100755 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,10 @@ coverage dist *.log .* +!.babelrc +!.editorconfig +!.eslintrc !.gitignore !.npmignore -!.babelrc +!.README !.travis.yml diff --git a/.travis.yml b/.travis.yml index e0fced4..a786167 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,14 @@ language: node_js node_js: - - '4.1' - - '4.0' + - node + - 8 +before_install: + - npm config set depth 0 notifications: - email: false + email: false sudo: false +script: + - npm run test + - npm run lint +after_success: + - semantic-release pre && npm publish && semantic-release post diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..109eb2e --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,5 @@ + diff --git a/README.md b/README.md index 88a335c..a6b7399 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,24 @@ # React CSS Modules +[![GitSpo Mentions](https://gitspo.com/badges/mentions/gajus/react-css-modules?style=flat-square)](https://gitspo.com/mentions/gajus/react-css-modules) [![Travis build status](http://img.shields.io/travis/gajus/react-css-modules/master.svg?style=flat-square)](https://travis-ci.org/gajus/react-css-modules) [![NPM version](http://img.shields.io/npm/v/react-css-modules.svg?style=flat-square)](https://www.npmjs.org/package/react-css-modules) +[![js-canonical-style](https://img.shields.io/badge/code%20style-canonical-blue.svg?style=flat-square)](https://github.com/gajus/canonical) - + React CSS Modules implement automatic mapping of CSS modules. Every CSS class is assigned a local-scoped identifier with a global unique name. CSS Modules enable a modular and reusable CSS! +> ## ⚠️⚠️⚠️ DEPRECATION NOTICE ⚠️⚠️⚠️ +> +> If you are considering to use `react-css-modules`, evaluate if [`babel-plugin-react-css-modules`](https://github.com/gajus/babel-plugin-react-css-modules) covers your use case. +> `babel-plugin-react-css-modules` is a lightweight alternative of `react-css-modules`. +> +> `babel-plugin-react-css-modules` is not a drop-in replacement and does not cover all the use cases of `react-css-modules`. +> However, it has a lot smaller performance overhead (0-10% vs +50%; see [Performance](https://github.com/gajus/babel-plugin-react-css-modules#performance)) and a lot smaller size footprint (less than 2kb vs +17kb). +> +> It is easy to get started! See the demo https://github.com/gajus/babel-plugin-react-css-modules/tree/master/demo + - [CSS Modules](#css-modules) - [webpack `css-loader`](#webpack-css-loader) - [What's the Problem?](#whats-the-problem) @@ -14,23 +26,27 @@ React CSS Modules implement automatic mapping of CSS modules. Every CSS class is - [Usage](#usage) - [Module Bundler](#module-bundler) - [webpack](#webpack) + - [Development](#development) + - [Production](#production) - [Browserify](#browserify) - [Extending Component Styles](#extending-component-styles) - - [Styles Property](#styles-property) + - [`styles` Property](#styles-property) + - [Loops and Child Components](#loops-and-child-components) - [Decorator](#decorator) - [Options](#options) - [`allowMultiple`](#allowmultiple) - - [`errorWhenNotFound`](#errorwhennotfound) + - [`handleNotFoundStyleName`](#handlenotfoundstylename) +- [SASS, SCSS, LESS and other CSS Preprocessors](#sass-scss-less-and-other-css-preprocessors) + - [Enable Sourcemaps](#enable-sourcemaps) - [Class Composition](#class-composition) - [What Problems does Class Composition Solve?](#what-problems-does-class-composition-solve) - [Class Composition Using CSS Preprocessors](#class-composition-using-css-preprocessors) -- [SASS, SCSS, LESS and other CSS Preprocessors](#sass-scss-less-and-other-css-preprocessors) - [Global CSS](#global-css) - [Multiple CSS Modules](#multiple-css-modules) ## CSS Modules -[CSS Modules](https://github.com/css-modules/css-modules) are awesome. If you are not familiar with CSS Modules, it is a concept of using a module bundler such as [webpack](http://webpack.github.io/docs/) to load CSS scoped to a particular document. CSS module loader will generate a unique name for a each CSS class at the time of loading the CSS document ([Interoperable CSS](https://github.com/css-modules/icss) to be precise). To see CSS Modules in practice, [webpack-demo](https://css-modules.github.io/webpack-demo/). +[CSS Modules](https://github.com/css-modules/css-modules) are awesome. If you are not familiar with CSS Modules, it is a concept of using a module bundler such as [webpack](http://webpack.github.io/docs/) to load CSS scoped to a particular document. CSS module loader will generate a unique name for each CSS class at the time of loading the CSS document ([Interoperable CSS](https://github.com/css-modules/icss) to be precise). To see CSS Modules in practice, [webpack-demo](https://css-modules.github.io/webpack-demo/). In the context of React, CSS Modules look like this: @@ -55,7 +71,7 @@ Rendering the component will produce a markup similar to: ```js
-
A0
+
A0
B0
@@ -67,7 +83,7 @@ Awesome! ### webpack `css-loader` -[CSS Modules](https://github.com/css-modules/css-modules) is a specification that can be implemented in multiple ways. `react-css-modules` leverages as existing CSS Modules implementation webpack [css-loader](https://github.com/webpack/css-loader#css-modules). +[CSS Modules](https://github.com/css-modules/css-modules) is a specification that can be implemented in multiple ways. `react-css-modules` leverages the existing CSS Modules implementation webpack [css-loader](https://github.com/webpack/css-loader#css-modules). ## What's the Problem? @@ -109,7 +125,7 @@ Using `react-css-modules`:
``` -* You are warned when `styleName` refers to an undefined CSS Module ([`errorWhenNotFound`](#errorwhennotfound) option). +* You are warned when `styleName` refers to an undefined CSS Module ([`handleNotFoundStyleName`](#handlenotfoundstylename) option). * You can enforce use of a single CSS module per `ReactElement` ([`allowMultiple`](#allowmultiple) option). ## The Implementation @@ -122,35 +138,106 @@ Using `react-css-modules`: Setup consists of: -* Setting up a [module bundler](#modulebundler) to load the [Interoperable CSS](https://github.com/css-modules/icss). +* Setting up a [module bundler](#module-bundler) to load the [Interoperable CSS](https://github.com/css-modules/icss). * [Decorating](#decorator) your component using `react-css-modules`. ### Module Bundler #### webpack -* Install [`style-loader`](https://www.npmjs.com/package/style-loader) and [`css-loader`](https://www.npmjs.com/package/css-loader). -* You need to use [`extract-text-webpack-plugin`](https://www.npmjs.com/package/extract-text-webpack-plugin) to aggregate the CSS into a single file. +##### Development + +In development environment, you want to [Enable Sourcemaps](#enable-sourcemaps) and webpack [Hot Module Replacement](https://webpack.github.io/docs/hot-module-replacement.html) (HMR). [`style-loader`](https://github.com/webpack/style-loader) already supports HMR. Therefore, Hot Module Replacement will work out of the box. + +Setup: + +* Install [`style-loader`](https://www.npmjs.com/package/style-loader). +* Install [`css-loader`](https://www.npmjs.com/package/css-loader). * Setup `/\.css$/` loader: ```js { test: /\.css$/, - loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]') + loaders: [ + 'style-loader?sourceMap', + 'css-loader?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]' + ] } ``` +##### Production + +In production environment, you want to extract chunks of CSS into a single stylesheet file. + +> Advantages: +> +> * Fewer style tags (older IE has a limit) +> * CSS SourceMap (with `devtool: "source-map"` and `css-loader?sourceMap`) +> * CSS requested in parallel +> * CSS cached separate +> * Faster runtime (less code and DOM operations) +> +> Caveats: +> +> * Additional HTTP request +> * Longer compilation time +> * More complex configuration +> * No runtime public path modification +> * No Hot Module Replacement + +– [extract-text-webpack-plugin](https://github.com/webpack/extract-text-webpack-plugin) + +Setup: + +* Install [`style-loader`](https://www.npmjs.com/package/style-loader). +* Install [`css-loader`](https://www.npmjs.com/package/css-loader). +* Use [`extract-text-webpack-plugin`](https://www.npmjs.com/package/extract-text-webpack-plugin) to extract chunks of CSS into a single stylesheet. + +* Setup `/\.css$/` loader: + + * ExtractTextPlugin v1x: + + ```js + { + test: /\.css$/, + loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]') + } + ``` + + * ExtractTextPlugin v2x: + + ```js + { + test: /\.css$/, + use: ExtractTextPlugin.extract({ + fallback: 'style-loader', + use: 'css-loader?modules,localIdentName="[name]-[local]-[hash:base64:6]"' + }), + } + ``` + * Setup `extract-text-webpack-plugin` plugin: -```js -new ExtractTextPlugin('app.css', { - allChunks: true -}) -``` + * ExtractTextPlugin v1x: + + ```js + new ExtractTextPlugin('app.css', { + allChunks: true + }) + ``` + + * ExtractTextPlugin v2x: + + ```js + new ExtractTextPlugin({ + filename: 'app.css', + allChunks: true + }) + ``` Refer to [webpack-demo](https://github.com/css-modules/webpack-demo) or [react-css-modules-examples](https://github.com/gajus/react-css-modules-examples) for an example of a complete setup. -#### Browserify +##### Browserify Refer to [`css-modulesify`](https://github.com/css-modules/css-modulesify). @@ -218,45 +305,99 @@ In this example, `table-custom-styles.css` selectively extends `table.css` (the Refer to the [`UsingStylesProperty` example](https://github.com/gajus/react-css-modules-examples/tree/master/src/UsingStylesProperty) for an example of a working implementation. -### Styles Property +### `styles` Property Decorated components inherit `styles` property that describes the mapping between CSS modules and CSS classes. -`styles` property within the component itself is designed to be used only when `styleNames` cannot be used. +```js +class extends React.Component { + render () { +
+

+

+
; + } +} +``` + +In the above example, `styleName='foo'` and `className={this.props.styles.foo}` are equivalent. + +`styles` property is designed to enable component decoration of [Loops and Child Components](#loops-and-child-components). -`styleNames` cannot be used within a component to define styles of a `ReactElement` that will be generated by another component (https://github.com/gajus/react-css-modules/issues/11), e.g. +### Loops and Child Components + +`styleName` cannot be used to define styles of a `ReactElement` that will be generated by another component, e.g. ```js -class extends React.Component { +import React from 'react'; +import CSSModules from 'react-css-modules'; +import List from './List'; +import styles from './table.css'; + +class CustomList extends React.Component { render () { let itemTemplate; itemTemplate = (name) => { - return
  • {name}
  • ; + return
  • {name}
  • ; }; - return ; + return ; } } + +export default CSSModules(CustomList, styles); ``` -For that purpose, the decorated component inherits `styles` property that you can use just as a regular CSS Modules object. The earlier example can be therefore rewritten to: +The above example will not work. `CSSModules` is used to decorate `CustomList` component. However, it is the `List` component that will render `itemTemplate`. + +For that purpose, the decorated component inherits [`styles` property](#styles-property) that you can use just as a regular CSS Modules object. The earlier example can be therefore rewritten to: ```js -class extends React.Component { +import React from 'react'; +import CSSModules from 'react-css-modules'; +import List from './List'; +import styles from './table.css'; + +class CustomList extends React.Component { render () { let itemTemplate; itemTemplate = (name) => { - return
  • {name}
  • ; + return
  • {name}
  • ; }; - return ; + return ; } } + +export default CSSModules(CustomList, styles); ``` -`styles` property works with ES6 classes and stateless function components. +You can use `styleName` property within the child component if you decorate the child component using `CSSModules` before passing it to the rendering component, e.g. + +```js +import React from 'react'; +import CSSModules from 'react-css-modules'; +import List from './List'; +import styles from './table.css'; + +class CustomList extends React.Component { + render () { + let itemTemplate; + + itemTemplate = (name) => { + return
  • {name}
  • ; + }; + + itemTemplate = CSSModules(itemTemplate, this.props.styles); + + return ; + } +} + +export default CSSModules(CustomList, styles); +``` ### Decorator @@ -265,7 +406,7 @@ class extends React.Component { * @typedef CSSModules~Options * @see {@link https://github.com/gajus/react-css-modules#options} * @property {Boolean} allowMultiple - * @property {Boolean} errorWhenNotFound + * @property {String} handleNotFoundStyleName */ /** @@ -349,11 +490,17 @@ When `false`, the following will cause an error:
    ``` -#### `errorWhenNotFound` +#### `handleNotFoundStyleName` + +Default: `throw`. -Default: `true`. +Defines the desired action when `styleName` cannot be mapped to an existing CSS Module. -Throws an error when `styleName` cannot be mapped to an existing CSS Module. +Available options: + +* `throw` throws an error +* `log` logs a warning using `console.warn` +* `ignore` silently ignores the missing style name ## SASS, SCSS, LESS and other CSS Preprocessors @@ -362,7 +509,28 @@ Throws an error when `styleName` cannot be mapped to an existing CSS Module. ```js { test: /\.scss$/, - loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!sass') + loaders: [ + 'style', + 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]', + 'resolve-url', + 'sass' + ] +} +``` + +### Enable Sourcemaps + +To enable CSS Source maps, add `sourceMap` parameter to the css-loader and to the `sass-loader`: + +```js +{ + test: /\.scss$/, + loaders: [ + 'style?sourceMap', + 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]', + 'resolve-url', + 'sass?sourceMap' + ] } ``` @@ -422,7 +590,7 @@ This pattern emerged with the advent of OOCSS. The biggest disadvantage of this ### Class Composition Using CSS Preprocessors -This section of the document is included as a learning exercise to broaden the understanding about the origin of Class Composition. CSS Modules support a native method of composting CSS Modules using [`composes`](https://github.com/css-modules/css-modules#composition) keyword. CSS Preprocessor is not required. +This section of the document is included as a learning exercise to broaden the understanding about the origin of Class Composition. CSS Modules support a native method of composing CSS Modules using [`composes`](https://github.com/css-modules/css-modules#composition) keyword. CSS Preprocessor is not required. You can write compositions in SCSS using [`@extend`](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#extend) keyword and using [Mixin Directives](http://sass-lang.com/documentation/file.SASS_REFERENCE.html#mixins), e.g. diff --git a/package.json b/package.json index 124ba62..00e1ffa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-css-modules", "description": "Seamless mapping of class names to CSS modules inside of React components.", - "main": "./dist/", + "main": "./dist/index.js", "repository": { "type": "git", "url": "https://github.com/gajus/react-css-modules" @@ -12,7 +12,7 @@ "css", "modules" ], - "version": "3.6.3", + "version": "4.3.0", "author": { "name": "Gajus Kuizinas", "email": "gajus@gajus.com", @@ -20,23 +20,36 @@ }, "license": "BSD-3-Clause", "dependencies": { - "lodash": "^3.10.1" + "hoist-non-react-statics": "^2.5.5", + "lodash": "^4.16.6", + "object-unfreeze": "^1.1.0" }, "devDependencies": { - "chai": "^3.4.1", - "jsdom": "^7.1.1", - "pragmatist": "^1.9.4", - "react": "^0.14.3", - "react-addons-test-utils": "^0.14.3" + "babel-cli": "^6.18.0", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-lodash": "^3.2.9", + "babel-plugin-transform-proto-to-assign": "^6.9.0", + "babel-preset-es2015": "^6.18.0", + "babel-preset-react": "^6.16.0", + "babel-preset-stage-0": "^6.16.0", + "babel-register": "^6.18.0", + "chai": "^4.0.0-canary.1", + "chai-spies": "^0.7.1", + "eslint": "^3.10.0", + "eslint-config-canonical": "^5.5.0", + "husky": "^0.11.9", + "jsdom": "^9.8.3", + "mocha": "^3.1.2", + "react": "^15.4.0-rc.4", + "react-addons-shallow-compare": "^15.4.0-rc.4", + "react-addons-test-utils": "^15.4.0-rc.4", + "react-dom": "^15.4.0-rc.4", + "semantic-release": "^6.3.2" }, "scripts": { - "pragmatist": "node ./node_modules/.bin/pragmatist", - "lint": "npm run pragmatist lint", - "test": "npm run pragmatist test", - "build": "npm run pragmatist build", - "watch": "npm run pragmatist watch", - "watch-lint": "npm run pragmatist watch-lint", - "watch-test": "npm run pragmatist watch-test", - "watch-build": "npm run pragmatist watch-build" + "lint": "eslint ./src ./tests", + "test": "NODE_ENV=development mocha --compilers js:babel-register ./tests/**/*.js && npm run lint && npm run build", + "build": "NODE_ENV=production babel ./src --out-dir ./dist", + "precommit": "npm run test" } } diff --git a/src/SimpleMap.js b/src/SimpleMap.js new file mode 100644 index 0000000..7cf618b --- /dev/null +++ b/src/SimpleMap.js @@ -0,0 +1,21 @@ +export default class { + constructor () { + this.size = 0; + this.keys = []; + this.values = []; + } + + get (key) { + const index = this.keys.indexOf(key); + + return this.values[index]; + } + + set (key, value) { + this.keys.push(key); + this.values.push(value); + this.size = this.keys.length; + + return value; + } +} diff --git a/src/extendReactClass.js b/src/extendReactClass.js index 556334d..4741bea 100644 --- a/src/extendReactClass.js +++ b/src/extendReactClass.js @@ -1,10 +1,10 @@ /* eslint-disable react/prop-types */ -import linkClass from './linkClass'; -import React from 'react'; import _ from 'lodash'; - -let extendReactClass; +import React from 'react'; +import hoistNonReactStatics from 'hoist-non-react-statics'; +import linkClass from './linkClass'; +import renderNothing from './renderNothing'; /** * @param {ReactClass} Component @@ -12,33 +12,63 @@ let extendReactClass; * @param {Object} options * @returns {ReactClass} */ -extendReactClass = (Component, defaultStyles, options) => { - return class extends Component { - render () { - let renderResult, - styles; - - if (this.props.styles) { - styles = this.props.styles; - } else if (_.isObject(defaultStyles)) { - this.props = _.assign({}, this.props, { - styles: defaultStyles - }); - - styles = defaultStyles; - } else { - styles = {}; - } - - renderResult = super.render(); - - if (renderResult) { - return linkClass(renderResult, styles, options); - } - - return React.createElement('noscript'); +export default (Component: Object, defaultStyles: Object, options: Object) => { + const WrappedComponent = class extends Component { + render () { + let styles; + + const hasDefaultstyles = _.isObject(defaultStyles); + + let renderResult; + + if (this.props.styles || hasDefaultstyles) { + const props = Object.assign({}, this.props); + + if (props.styles) { + styles = props.styles; + } else if (hasDefaultstyles) { + styles = defaultStyles; + delete props.styles; } - }; -}; -export default extendReactClass; + Object.defineProperty(props, 'styles', { + configurable: true, + enumerable: false, + value: styles, + writable: false + }); + + const originalProps = this.props; + + let renderIsSuccessful = false; + + try { + this.props = props; + + renderResult = super.render(); + + renderIsSuccessful = true; + } finally { + this.props = originalProps; + } + + // @see https://github.com/facebook/react/issues/14224 + if (!renderIsSuccessful) { + renderResult = super.render(); + } + } else { + styles = {}; + + renderResult = super.render(); + } + + if (renderResult) { + return linkClass(renderResult, styles, options); + } + + return renderNothing(React.version); + } + }; + + return hoistNonReactStatics(WrappedComponent, Component); +}; diff --git a/src/generateAppendClassName.js b/src/generateAppendClassName.js new file mode 100644 index 0000000..127aa2a --- /dev/null +++ b/src/generateAppendClassName.js @@ -0,0 +1,49 @@ +import SimpleMap from './SimpleMap'; + +const CustomMap = typeof Map === 'undefined' ? SimpleMap : Map; + +const stylesIndex = new CustomMap(); + +export default (styles, styleNames: Array, handleNotFoundStyleName: "throw" | "log" | "ignore"): string => { + let appendClassName; + let stylesIndexMap; + + stylesIndexMap = stylesIndex.get(styles); + + if (stylesIndexMap) { + const styleNameIndex = stylesIndexMap.get(styleNames); + + if (styleNameIndex) { + return styleNameIndex; + } + } else { + stylesIndexMap = new CustomMap(); + stylesIndex.set(styles, new CustomMap()); + } + + appendClassName = ''; + + for (const styleName in styleNames) { + if (styleNames.hasOwnProperty(styleName)) { + const className = styles[styleNames[styleName]]; + + if (className) { + appendClassName += ' ' + className; + } else { + if (handleNotFoundStyleName === 'throw') { + throw new Error('"' + styleNames[styleName] + '" CSS module is undefined.'); + } + if (handleNotFoundStyleName === 'log') { + // eslint-disable-next-line no-console + console.warn('"' + styleNames[styleName] + '" CSS module is undefined.'); + } + } + } + } + + appendClassName = appendClassName.trim(); + + stylesIndexMap.set(styleNames, appendClassName); + + return appendClassName; +}; diff --git a/src/index.js b/src/index.js index 9e8c274..b913b29 100644 --- a/src/index.js +++ b/src/index.js @@ -1,64 +1,56 @@ import _ from 'lodash'; import extendReactClass from './extendReactClass'; import wrapStatelessFunction from './wrapStatelessFunction'; +import makeConfiguration from './makeConfiguration'; -let decoratorConstructor, - functionConstructor, - isReactComponent; +/** + * @see https://github.com/gajus/react-css-modules#options + */ +type TypeOptions = {}; /** * Determines if the given object has the signature of a class that inherits React.Component. - * - * @param {*} Component - * @return {boolean} */ -isReactComponent = (Component) => { - return 'prototype' in Component && _.isFunction(Component.prototype.render); +const isReactComponent = (maybeReactComponent: any): boolean => { + return 'prototype' in maybeReactComponent && _.isFunction(maybeReactComponent.prototype.render); }; /** * When used as a function. - * - * @param {Function} Component - * @param {Object} defaultStyles CSS Modules class map. - * @param {Object} options {@link https://github.com/gajus/react-css-modules#options} - * @return {Function} */ -functionConstructor = (Component, defaultStyles, options) => { - let decoratedClass; +const functionConstructor = (Component: Function, defaultStyles: Object, options: TypeOptions): Function => { + let decoratedClass; + + const configuration = makeConfiguration(options); - if (isReactComponent(Component)) { - decoratedClass = extendReactClass(Component, defaultStyles, options); - } else { - decoratedClass = wrapStatelessFunction(Component, defaultStyles, options); - } + if (isReactComponent(Component)) { + decoratedClass = extendReactClass(Component, defaultStyles, configuration); + } else { + decoratedClass = wrapStatelessFunction(Component, defaultStyles, configuration); + } - if (Component.displayName) { - decoratedClass.displayName = Component.displayName; - } else { - decoratedClass.displayName = Component.name; - } + if (Component.displayName) { + decoratedClass.displayName = Component.displayName; + } else { + decoratedClass.displayName = Component.name; + } - return decoratedClass; + return decoratedClass; }; /** * When used as a ES7 decorator. - * - * @param {Object} defaultStyles CSS Modules class map. - * @param {Object} options {@link https://github.com/gajus/react-css-modules#options} - * @return {Function} */ -decoratorConstructor = (defaultStyles, options) => { - return (Component) => { - return functionConstructor(Component, defaultStyles, options); - }; +const decoratorConstructor = (defaultStyles: Object, options: TypeOptions): Function => { + return (Component: Function) => { + return functionConstructor(Component, defaultStyles, options); + }; }; export default (...args) => { - if (_.isFunction(args[0])) { - return functionConstructor(args[0], args[1], args[2]); - } else { - return decoratorConstructor(args[0], args[1]); - } + if (_.isFunction(args[0])) { + return functionConstructor(args[0], args[1], args[2]); + } else { + return decoratorConstructor(args[0], args[1]); + } }; diff --git a/src/isIterable.js b/src/isIterable.js index 9e4c449..67d0a94 100644 --- a/src/isIterable.js +++ b/src/isIterable.js @@ -1,29 +1,24 @@ import _ from 'lodash'; -let ITERATOR_SYMBOL, - OLD_ITERATOR_SYMBOL; - -ITERATOR_SYMBOL = _.isFunction(Symbol) && Symbol.iterator; -OLD_ITERATOR_SYMBOL = '@@iterator'; +const ITERATOR_SYMBOL = typeof Symbol !== 'undefined' && _.isFunction(Symbol) && Symbol.iterator; +const OLD_ITERATOR_SYMBOL = '@@iterator'; /** * @see https://github.com/lodash/lodash/issues/1668 * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Iteration_protocols - * @param {Object} target - * @returns {boolean} */ -export default (target) => { - let iterator; +export default (maybeIterable: any): boolean => { + let iterator; - if (!_.isObject(target)) { - return false; - } + if (!_.isObject(maybeIterable)) { + return false; + } - if (ITERATOR_SYMBOL) { - iterator = target[ITERATOR_SYMBOL]; - } else { - iterator = target[OLD_ITERATOR_SYMBOL]; - } + if (ITERATOR_SYMBOL) { + iterator = maybeIterable[ITERATOR_SYMBOL]; + } else { + iterator = maybeIterable[OLD_ITERATOR_SYMBOL]; + } - return _.isFunction(iterator); + return _.isFunction(iterator); }; diff --git a/src/linkClass.js b/src/linkClass.js index ce824e2..4ed3975 100644 --- a/src/linkClass.js +++ b/src/linkClass.js @@ -1,103 +1,106 @@ -import React from 'react'; -import makeConfiguration from './makeConfiguration'; -import isIterable from './isIterable'; import _ from 'lodash'; - -let linkClass; - -/** - * @param {ReactElement} element - * @param {Object} styles CSS modules class map. - * @param {CSSModules~Options} userConfiguration - * @return {ReactElement} - */ -linkClass = (element, styles = {}, userConfiguration) => { - let appendClassName, - children, - clonedElement, - configuration, - newChildren, - newProps, - styleNames; - - // @see https://github.com/gajus/react-css-modules/pull/30 - if (!element) { - return element; +import React, { + ReactElement +} from 'react'; +import objectUnfreeze from 'object-unfreeze'; +import isIterable from './isIterable'; +import parseStyleName from './parseStyleName'; +import generateAppendClassName from './generateAppendClassName'; + +const linkArray = (array: Array, styles: Object, configuration: Object) => { + _.forEach(array, (value, index) => { + if (React.isValidElement(value)) { + // eslint-disable-next-line no-use-before-define + array[index] = linkElement(React.Children.only(value), styles, configuration); + } else if (_.isArray(value)) { + const unfreezedValue = Object.isFrozen(value) ? objectUnfreeze(value) : value; + + array[index] = linkArray(unfreezedValue, styles, configuration); } + }); - configuration = makeConfiguration(userConfiguration); - - styleNames = element.props.styleName; - - if (styleNames) { - styleNames = styleNames.split(' '); - styleNames = _.filter(styleNames); - - if (configuration.allowMultiple === false && styleNames.length > 1) { - throw new Error('ReactElement styleName property defines multiple module names ("' + element.props.styleName + '").'); - } - - appendClassName = _.map(styleNames, (styleName) => { - if (styles[styleName]) { - return styles[styleName]; - } else { - if (configuration.errorWhenNotFound === true) { - throw new Error('"' + styleName + '" CSS module is undefined.'); - } - - return ''; - } - }); - - appendClassName = _.filter(appendClassName, 'length'); + return array; +}; - appendClassName = appendClassName.join(' '); +const linkElement = (element: ReactElement, styles: Object, configuration: Object): ReactElement => { + let appendClassName; + let elementShallowCopy; + + elementShallowCopy = element; + + if (Array.isArray(elementShallowCopy)) { + return elementShallowCopy.map((arrayElement) => { + return linkElement(arrayElement, styles, configuration); + }); + } + + const elementIsFrozen = Object.isFrozen && Object.isFrozen(elementShallowCopy); + const propsFrozen = Object.isFrozen && Object.isFrozen(elementShallowCopy.props); + const propsNotExtensible = Object.isExtensible && !Object.isExtensible(elementShallowCopy.props); + + if (elementIsFrozen) { + // https://github.com/facebook/react/blob/v0.13.3/src/classic/element/ReactElement.js#L131 + elementShallowCopy = objectUnfreeze(elementShallowCopy); + elementShallowCopy.props = objectUnfreeze(elementShallowCopy.props); + } else if (propsFrozen || propsNotExtensible) { + elementShallowCopy.props = objectUnfreeze(elementShallowCopy.props); + } + + const styleNames = parseStyleName(elementShallowCopy.props.styleName || '', configuration.allowMultiple); + const {children, ...restProps} = elementShallowCopy.props; + + if (React.isValidElement(children)) { + elementShallowCopy.props.children = linkElement(React.Children.only(children), styles, configuration); + } else if (_.isArray(children) || isIterable(children)) { + elementShallowCopy.props.children = linkArray(objectUnfreeze(children), styles, configuration); + } + + _.forEach(restProps, (propValue, propName) => { + if (React.isValidElement(propValue)) { + elementShallowCopy.props[propName] = linkElement(React.Children.only(propValue), styles, configuration); + } else if (_.isArray(propValue)) { + elementShallowCopy.props[propName] = linkArray(propValue, styles, configuration); } + }); - // element.props.children can be one of the following: - // 'text' - // ['text'] - // [ReactElement, 'text'] - // ReactElement - - children = element.props.children; - - if (React.isValidElement(children)) { - newChildren = linkClass(React.Children.only(children), styles, configuration); - } else if (_.isArray(children) || isIterable(children)) { - /* eslint-disable lodash3/prefer-lodash-method */ - newChildren = React.Children.map(children, (node) => { - /* eslint-enable lodash3/prefer-lodash-method */ - if (React.isValidElement(node)) { - return linkClass(node, styles, configuration); - } else { - return node; - } - }); - - // https://github.com/facebook/react/issues/4723#issuecomment-135555277 - // Forcing children into an array produces the following error: - // Warning: A ReactFragment is an opaque type. Accessing any of its properties is deprecated. Pass it to one of the React.Children helpers. - // newChildren = _.values(newChildren); - } + if (styleNames.length) { + appendClassName = generateAppendClassName(styles, styleNames, configuration.handleNotFoundStyleName); if (appendClassName) { - if (element.props.className) { - appendClassName = element.props.className + ' ' + appendClassName; - } + if (elementShallowCopy.props.className) { + appendClassName = elementShallowCopy.props.className + ' ' + appendClassName; + } - newProps = { - className: appendClassName - }; + elementShallowCopy.props.className = appendClassName; } + } - if (newChildren) { - clonedElement = React.cloneElement(element, newProps, newChildren); - } else { - clonedElement = React.cloneElement(element, newProps); - } + delete elementShallowCopy.props.styleName; + + if (elementIsFrozen) { + Object.freeze(elementShallowCopy.props); + Object.freeze(elementShallowCopy); + } else if (propsFrozen) { + Object.freeze(elementShallowCopy.props); + } + + if (propsNotExtensible) { + Object.preventExtensions(elementShallowCopy.props); + } - return clonedElement; + return elementShallowCopy; }; -export default linkClass; +/** + * @param {ReactElement} element + * @param {Object} styles CSS modules class map. + * @param {CSSModules~Options} configuration + */ +export default (element: ReactElement, styles = {}, configuration = {}): ReactElement => { + // @see https://github.com/gajus/react-css-modules/pull/30 + if (!_.isObject(element)) { + return element; + } + + return linkElement(element, styles, configuration); +}; diff --git a/src/makeConfiguration.js b/src/makeConfiguration.js index 292e2f1..cc3169c 100644 --- a/src/makeConfiguration.js +++ b/src/makeConfiguration.js @@ -4,32 +4,34 @@ import _ from 'lodash'; * @typedef CSSModules~Options * @see {@link https://github.com/gajus/react-css-modules#options} * @property {boolean} allowMultiple - * @property {boolean} errorWhenNotFound + * @property {string} handleNotFoundStyleName */ /** * @param {CSSModules~Options} userConfiguration - * @return {CSSModules~Options} + * @returns {CSSModules~Options} */ export default (userConfiguration = {}) => { - let configuration; + const configuration = { + allowMultiple: false, + handleNotFoundStyleName: 'throw' + }; - configuration = { - allowMultiple: false, - errorWhenNotFound: true - }; + _.forEach(userConfiguration, (value, name) => { + if (_.isUndefined(configuration[name])) { + throw new Error('Unknown configuration property "' + name + '".'); + } - _.forEach(userConfiguration, (value, name) => { - if (_.isUndefined(configuration[name])) { - throw new Error('Unknown configuration property "' + name + '".'); - } + if (name === 'allowMultiple' && !_.isBoolean(value)) { + throw new Error('"allowMultiple" property value must be a boolean.'); + } - if (!_.isBoolean(value)) { - throw new Error('"' + name + '" property value must be a boolean.'); - } + if (name === 'handleNotFoundStyleName' && !_.includes(['throw', 'log', 'ignore'], value)) { + throw new Error('"handleNotFoundStyleName" property value must be "throw", "log" or "ignore".'); + } - configuration[name] = value; - }); + configuration[name] = value; + }); - return configuration; + return configuration; }; diff --git a/src/parseStyleName.js b/src/parseStyleName.js new file mode 100644 index 0000000..937b2d6 --- /dev/null +++ b/src/parseStyleName.js @@ -0,0 +1,22 @@ +import _ from 'lodash'; + +const styleNameIndex = {}; + +export default (styleNamePropertyValue: string, allowMultiple: boolean): Array => { + let styleNames; + + if (styleNameIndex[styleNamePropertyValue]) { + styleNames = styleNameIndex[styleNamePropertyValue]; + } else { + styleNames = _.trim(styleNamePropertyValue).split(/\s+/); + styleNames = _.filter(styleNames); + + styleNameIndex[styleNamePropertyValue] = styleNames; + } + + if (allowMultiple === false && styleNames.length > 1) { + throw new Error('ReactElement styleName property defines multiple module names ("' + styleNamePropertyValue + '").'); + } + + return styleNames; +}; diff --git a/src/renderNothing.js b/src/renderNothing.js new file mode 100644 index 0000000..93675fe --- /dev/null +++ b/src/renderNothing.js @@ -0,0 +1,7 @@ +import React from 'react'; + +export default function (version) { + const major = version.split('.')[0]; + + return parseInt(major, 10) < 15 ? React.createElement('noscript') : null; +} diff --git a/src/wrapStatelessFunction.js b/src/wrapStatelessFunction.js index 342d724..b6b9162 100644 --- a/src/wrapStatelessFunction.js +++ b/src/wrapStatelessFunction.js @@ -1,52 +1,49 @@ /* eslint-disable react/prop-types */ -import linkClass from './linkClass'; -import React from 'react'; import _ from 'lodash'; - -let wrapStatelessFunction; +import React from 'react'; +import linkClass from './linkClass'; +import renderNothing from './renderNothing'; /** * @see https://facebook.github.io/react/blog/2015/09/10/react-v0.14-rc1.html#stateless-function-components - * @param {Function} Component - * @param {Object} defaultStyles - * @param {Object} options - * @returns {Function} */ -wrapStatelessFunction = (Component, defaultStyles, options) => { - let WrappedComponent; - - WrappedComponent = (props = {}, ...args) => { - let renderResult, - styles, - useProps; - - if (props.styles) { - useProps = props; - styles = props.styles; - } else if (_.isObject(defaultStyles)) { - useProps = _.assign({}, props, { - styles: defaultStyles - }); - - styles = defaultStyles; - } else { - useProps = props; - styles = {}; - } - - renderResult = Component(useProps, ...args); - - if (renderResult) { - return linkClass(renderResult, styles, options); - } - - return React.createElement('noscript'); - }; - - _.assign(WrappedComponent, Component); - - return WrappedComponent; +export default (Component: Function, defaultStyles: Object, options: Object): Function => { + const WrappedComponent = (props = {}, ...args) => { + let styles; + let useProps; + const hasDefaultstyles = _.isObject(defaultStyles); + + if (props.styles || hasDefaultstyles) { + useProps = Object.assign({}, props); + + if (props.styles) { + styles = props.styles; + } else { + styles = defaultStyles; + } + + Object.defineProperty(useProps, 'styles', { + configurable: true, + enumerable: false, + value: styles, + writable: false + }); + } else { + useProps = props; + styles = {}; + } + + const renderResult = Component(useProps, ...args); + + if (renderResult) { + return linkClass(renderResult, styles, options); + } + + return renderNothing(React.version); + }; + + _.assign(WrappedComponent, Component); + + return WrappedComponent; }; - -export default wrapStatelessFunction; diff --git a/tests/SimpleMap.js b/tests/SimpleMap.js new file mode 100644 index 0000000..08ca888 --- /dev/null +++ b/tests/SimpleMap.js @@ -0,0 +1,38 @@ +import { + expect +} from 'chai'; +import SimpleMap from './../src/SimpleMap'; + +describe('SimpleMap', () => { + context('simple map with primitive or object as keys', () => { + const values = [ + [1, 'something'], + ['1', 'somethingElse'], + [{}, []], + [null, null] + ]; + + let map; + + beforeEach(() => { + map = new SimpleMap(); + }); + + it('should set', () => { + values.forEach(([key, value]) => { + map.set(key, value); + }); + expect(map.size).to.equal(values.length); + }); + + it('should get', () => { + values.forEach(([key, value]) => { + map.set(key, value); + }); + + values.forEach(([key, value]) => { + expect(map.get(key)).to.equal(value); + }); + }); + }); +}); diff --git a/tests/extendReactClass.js b/tests/extendReactClass.js index 7f269dc..01733ee 100644 --- a/tests/extendReactClass.js +++ b/tests/extendReactClass.js @@ -1,81 +1,167 @@ -/* eslint-disable max-nested-callbacks */ +/* eslint-disable max-nested-callbacks, react/prefer-stateless-function, react/prop-types, react/no-multi-comp, class-methods-use-this */ import { expect } from 'chai'; - import React from 'react'; import TestUtils from 'react-addons-test-utils'; +import shallowCompare from 'react-addons-shallow-compare'; import jsdom from 'jsdom'; import extendReactClass from './../src/extendReactClass'; describe('extendReactClass', () => { - beforeEach(() => { - global.document = jsdom.jsdom(''); + beforeEach(() => { + global.document = jsdom.jsdom(''); + + global.window = document.defaultView; + }); + context('using default styles', () => { + it('exposes styles through this.props.styles property', (done) => { + let Component; + + const styles = { + foo: 'foo-1' + }; + + Component = class extends React.Component { + render () { + expect(this.props.styles).to.equal(styles); + done(); + } + }; - global.window = document.defaultView; + Component = extendReactClass(Component, styles); + + TestUtils.renderIntoDocument(); }); + it('exposes non-enumerable styles property', (done) => { + let Component; + + const styles = { + foo: 'foo-1' + }; + + Component = class extends React.Component { + render () { + expect(this.props.propertyIsEnumerable('styles')).to.equal(false); + done(); + } + }; + + Component = extendReactClass(Component, styles); - context('using default styles', () => { - it('exposes styles through styles property', (done) => { - let Component, - styles; - - Component = class extends React.Component { - render () { - expect(this.props.styles).to.equal(styles); - done(); - } - }; - - styles = { - foo: 'foo-1' - }; - - Component = extendReactClass(Component, styles); - - TestUtils.renderIntoDocument(); - }); - it('does not affect the other instance properties', (done) => { - let Component, - styles; - - Component = class extends React.Component { - render () { - expect(this.props.bar).to.equal('baz'); - done(); - } - }; - - styles = { - foo: 'foo-1' - }; - - Component = extendReactClass(Component, styles); - - TestUtils.renderIntoDocument(); - }); + TestUtils.renderIntoDocument(); }); + it('does not affect the other instance properties', (done) => { + let Component; + + Component = class extends React.Component { + render () { + expect(this.props.bar).to.equal('baz'); + done(); + } + }; + + const styles = { + foo: 'foo-1' + }; + + Component = extendReactClass(Component, styles); + + TestUtils.renderIntoDocument(); + }); + it('does not affect pure-render logic', (done) => { + let Component; + let rendered; + + rendered = false; + + const styles = { + foo: 'foo-1' + }; - context('using explicit styles', () => { - it('exposes styles through styles property', (done) => { - let Component, - styles; + Component = class extends React.Component { + shouldComponentUpdate (newProps) { + if (rendered) { + expect(shallowCompare(this.props, newProps)).to.equal(true); - Component = class extends React.Component { - render () { - expect(this.props.styles).to.equal(styles); - done(); - } - }; + done(); + } + + return true; + } + + render () { + rendered = true; + } + }; + + Component = extendReactClass(Component, styles); + + const instance = TestUtils.renderIntoDocument(); + + // trigger shouldComponentUpdate + instance.setState({}); + }); + }); + context('overwriting default styles using "styles" property of the extended component', () => { + it('overwrites default styles', (done) => { + let Component; + + const styles = { + foo: 'foo-1' + }; + + Component = class extends React.Component { + render () { + expect(this.props.styles).to.equal(styles); + done(); + } + }; + + Component = extendReactClass(Component, { + bar: 'bar-0', + foo: 'foo-0' + }); + + TestUtils.renderIntoDocument(); + }); + }); + context('rendering Component that returns null', () => { + it('generates null', () => { + let Component; + + const shallowRenderer = TestUtils.createRenderer(); + + Component = class extends React.Component { + render () { + return null; + } + }; + + Component = extendReactClass(Component); + + shallowRenderer.render(); + + const component = shallowRenderer.getRenderOutput(); + + expect(component).to.equal(null); + }); + }); + context('target component have static properties', () => { + it('hoists static properties', () => { + const Component = class extends React.Component { + static foo = 'FOO'; - styles = { - foo: 'foo-1' - }; + render () { + return null; + } + }; - Component = extendReactClass(Component); + const WrappedComponent = extendReactClass(Component); - TestUtils.renderIntoDocument(); - }); + expect(Component.foo).to.equal('FOO'); + expect(WrappedComponent.foo).to.equal(Component.foo); }); + }); }); diff --git a/tests/linkClass.js b/tests/linkClass.js index 1aa28f3..cf7c289 100644 --- a/tests/linkClass.js +++ b/tests/linkClass.js @@ -1,253 +1,483 @@ -/* eslint-disable max-nested-callbacks */ +/* eslint-disable max-nested-callbacks, react/prefer-stateless-function, class-methods-use-this, no-console, no-unused-expressions */ -import { +import chai, { expect } from 'chai'; - +import spies from 'chai-spies'; import React from 'react'; import TestUtils from 'react-addons-test-utils'; import jsdom from 'jsdom'; import linkClass from './../src/linkClass'; +chai.use(spies); + describe('linkClass', () => { - context('ReactElement does not define styleName', () => { - it('does not affect element properties', () => { - expect(linkClass(
    )).to.deep.equal(
    ); + context('ReactElement does not define styleName', () => { + it('does not affect element properties', () => { + expect(linkClass(
    )).to.deep.equal(
    ); + }); + + it('does not affect element properties with a single element child', () => { + expect(linkClass(

    )).to.deep.equal(

    ); + }); + + it('does not affect element properties with a single element child in non-`children` prop', () => { + expect(linkClass(
    } />)).to.deep.equal(
    } />); + }); + + it('does not affect element properties with a single text child', () => { + expect(linkClass(
    test
    )).to.deep.equal(
    test
    ); + }); + + it('does not affect the className', () => { + expect(linkClass(
    )).to.deep.equal(
    ); + }); + + xit('does not affect element with a single children when that children is contained in an array', () => { + const subject = React.createElement('div', null, [ + React.createElement('p') + ]); + const outcome = React.createElement('div', null, [ + React.createElement('p') + ]); + + expect(linkClass(subject)).to.deep.equal(outcome); + }); + + xit('does not affect element with multiple children', () => { + // Using array instead of object causes the following error: + // Warning: Each child in an array or iterator should have a unique "key" prop. + // Check the render method of _class. See https://fb.me/react-warning-keys for more information. + // @see https://github.com/facebook/react/issues/4723#issuecomment-135555277 + // expect(linkClass(

    )).to.deep.equal(

    ); + + const subject = React.createElement('div', null, [ + React.createElement('p'), + React.createElement('p') + ]); + const outcome = React.createElement('div', null, [ + React.createElement('p'), + React.createElement('p') + ]); + + expect(linkClass(subject)).to.deep.equal(outcome); + }); + }); + + context('called with null instead of ReactElement', () => { + it('returns null', () => { + const subject = linkClass(null); + + expect(subject).to.equal(null); + }); + }); + + context('styleName matches an existing CSS module', () => { + context('when a descendant element has styleName', () => { + it('assigns a generated className', () => { + let subject; + + subject =
    +

    +

    ; + + subject = linkClass(subject, { + foo: 'foo-1' }); - it('does not affect element properties with a single element child', () => { - expect(linkClass(

    )).to.deep.equal(

    ); + expect(subject.props.children.props.className).to.equal('foo-1'); + }); + }); + context('when a descendant element in non-`children` prop has styleName', () => { + it('assigns a generated className', () => { + let subject; + + subject =
    } + els={[

    , [

    ]]} + />; + + subject = linkClass(subject, { + bar: 'bar-1', + baz: 'baz-1', + foo: 'foo-1' }); - it('does not affect element properties with a single text child', () => { - expect(linkClass(

    test
    )).to.deep.equal(
    test
    ); + expect(subject.props.el.props.className).to.equal('foo-1'); + expect(subject.props.els[0].props.className).to.equal('bar-1'); + expect(subject.props.els[1][0].props.className).to.equal('baz-1'); + }); + }); + context('when multiple descendant elements have styleName', () => { + it('assigns a generated className', () => { + let subject; + + subject =
    +

    +

    +

    ; + + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' }); - it('does not affect the className', () => { - expect(linkClass(
    )).to.deep.equal(
    ); + expect(subject.props.children[0].props.className).to.equal('foo-1'); + expect(subject.props.children[1].props.className).to.equal('bar-1'); + }); + it('assigns a generated className to elements inside nested arrays', () => { + let subject; + + subject =
    + {[ + [ +

    , +

    + ], + [ +

    , +

    + ] + ]} +

    ; + + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' }); - xit('does not affect element with a single children when that children is contained in an array', () => { - let outcome, - subject; + expect(subject.props.children[0][0].props.className).to.equal('foo-1'); + expect(subject.props.children[0][1].props.className).to.equal('bar-1'); - subject = React.createElement('div', null, [ - React.createElement('p') - ]); - outcome = React.createElement('div', null, [ - React.createElement('p') - ]); + expect(subject.props.children[1][0].props.className).to.equal('foo-1'); + expect(subject.props.children[1][1].props.className).to.equal('bar-1'); + }); + it('styleName is deleted from props', () => { + let subject; - expect(linkClass(subject)).to.deep.equal(outcome); - }); + subject =
    +

    +

    +

    ; - xit('does not affect element with multiple children', () => { - // Using array instead of object causes the following error: - // Warning: Each child in an array or iterator should have a unique "key" prop. - // Check the render method of _class. See https://fb.me/react-warning-keys for more information. - // @see https://github.com/facebook/react/issues/4723#issuecomment-135555277 - // expect(linkClass(

    )).to.deep.equal(

    ); + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' + }); - let outcome, - subject; + expect(subject.props.children[0].props).not.to.have.property('styleName'); + expect(subject.props.children[1].props).not.to.have.property('styleName'); + }); + it('preserves original keys', () => { + let subject; - subject = React.createElement('div', null, [ - React.createElement('p'), - React.createElement('p') - ]); - outcome = React.createElement('div', null, [ - React.createElement('p'), - React.createElement('p') - ]); + subject =
    +

    +

    +

    ; - expect(linkClass(subject)).to.deep.equal(outcome); + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' }); + + expect(subject.props.children[0].key).to.equal('1'); + expect(subject.props.children[1].key).to.equal('2'); + }); }); + context('when multiple descendants have styleName and are iterable', () => { + it('assigns a generated className', () => { + let subject; + + const iterable = { + 0:

    , + 1:

    , + length: 2, - context('called with null instead of ReactElement', () => { - it('returns null', () => { - let subject; + // eslint-disable-next-line no-use-extend-native/no-use-extend-native + [Symbol.iterator]: Array.prototype[Symbol.iterator] + }; - subject = linkClass(null); + subject =

    {iterable}
    ; - expect(subject).to.equal(null); + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' }); + + expect(subject.props.children[0].props.className).to.equal('foo-1'); + expect(subject.props.children[1].props.className).to.equal('bar-1'); + }); }); + context('when non-`children` prop is an iterable', () => { + it('it is left untouched', () => { + let subject; - context('styleName matches an existing CSS module', () => { - context('when a descendant element has styleName', () => { - it('assigns a generated className', () => { - let subject; + const iterable = { + 0:

    , + 1:

    , + length: 2, - subject =

    -

    -
    ; + // eslint-disable-next-line no-use-extend-native/no-use-extend-native + [Symbol.iterator]: Array.prototype[Symbol.iterator] + }; - subject = linkClass(subject, { - foo: 'foo-1' - }); + subject =
    ; - expect(subject.props.children.props.className).to.equal('foo-1'); - }); + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' }); - context('when multiple descendant elements have styleName', () => { - it('assigns a generated className', () => { - let subject; - subject =
    -

    -

    -
    ; + expect(subject.props.els[0].props.styleName).to.equal('foo'); + expect(subject.props.els[1].props.styleName).to.equal('bar'); + expect(subject.props.els[0].props).not.to.have.property('className'); + expect(subject.props.els[1].props).not.to.have.property('className'); + }); + }); + context('when ReactElement does not have an existing className', () => { + it('uses the generated class name to set the className property', () => { + let subject; - subject = linkClass(subject, { - foo: 'foo-1', - bar: 'bar-1' - }); + subject =
    ; - expect(subject.props.children[0].props.className).to.equal('foo-1'); - expect(subject.props.children[1].props.className).to.equal('bar-1'); - }); + subject = linkClass(subject, { + foo: 'foo-1' }); - context('when multiple descendants have styleName and are iterable', () => { - it('assigns a generated className', () => { - let iterable, - subject; - - iterable = { - 0:

    , - 1:

    , - length: 2, - [Symbol.iterator]: Array.prototype[Symbol.iterator] - }; - subject =
    {iterable}
    ; + expect(subject.props.className).to.deep.equal('foo-1'); + }); + }); + context('when ReactElement has an existing className', () => { + it('appends the generated class name to the className property', () => { + let subject; - subject = linkClass(subject, { - foo: 'foo-1', - bar: 'bar-1' - }); + subject =
    ; - expect(subject.props.children[0].props.className).to.equal('foo-1'); - expect(subject.props.children[1].props.className).to.equal('bar-1'); - }); + subject = linkClass(subject, { + bar: 'bar-1' }); - context('when ReactElement does not have an existing className', () => { - it('uses the generated class name to set the className property', () => { - let subject; - subject =
    ; + expect(subject.props.className).to.deep.equal('foo bar-1'); + }); + }); + }); - subject = linkClass(subject, { - foo: 'foo-1' - }); + context('styleName includes multiple whitespace characters', () => { + it('resolves CSS modules', () => { + let subject; - expect(subject.props.className).to.deep.equal('foo-1'); - }); - }); - context('when ReactElement has an existing className', () => { - it('appends the generated class name to the className property', () => { - let subject; + subject =
    +

    +

    ; + + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' + }, { + allowMultiple: true + }); + + expect(subject.props.children.props.className).to.equal('foo-1 bar-1'); + }); + }); - subject =
    ; + context('can\'t write to properties', () => { + context('when the element is frozen', () => { + it('adds className but is still frozen', () => { + let subject; - subject = linkClass(subject, { - bar: 'bar-1' - }); + subject =
    ; - expect(subject.props.className).to.deep.equal('foo bar-1'); - }); + Object.freeze(subject); + subject = linkClass(subject, { + foo: 'foo-1' }); + + expect(subject).to.be.frozen; + expect(subject.props.className).to.equal('foo-1'); + }); }); + context('when the element\'s props are frozen', () => { + it('adds className and only props are still frozen', () => { + let subject; - context('styleName includes multiple whitespace characters', () => { - it('resolves CSS modules', () => { - let subject; + subject =
    ; - subject =
    -

    -
    ; + Object.freeze(subject.props); + subject = linkClass(subject, { + foo: 'foo-1' + }); - subject = linkClass(subject, { - foo: 'foo-1', - bar: 'bar-1' - }, { - allowMultiple: true - }); + expect(subject.props).to.be.frozen; + expect(subject.props.className).to.equal('foo-1'); + }); + }); + context('when the element\'s props are not extensible', () => { + it('adds className and props are still not extensible', () => { + let subject; - expect(subject.props.children.props.className).to.equal('foo-1 bar-1'); + subject =
    ; + + Object.preventExtensions(subject.props); + subject = linkClass(subject, { + foo: 'foo-1' }); + + expect(subject.props).to.not.be.extensible; + expect(subject.props.className).to.equal('foo-1'); + }); + }); + }); + + context('when element is an array', () => { + it('handles each element individually', () => { + let subject; + + subject = [ +
    , +
    +

    +

    + ]; + + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' + }); + + expect(subject).to.be.an('array'); + expect(subject[0].props.className).to.equal('foo-1'); + expect(subject[1].props.children.props.className).to.equal('bar-1'); }); + }); + + describe('options.allowMultiple', () => { + context('when multiple module names are used', () => { + context('when false', () => { + it('throws an error', () => { + expect(() => { + linkClass(
    , {}, {allowMultiple: false}); + }).to.throw(Error, 'ReactElement styleName property defines multiple module names ("foo bar").'); + }); + }); + context('when true', () => { + it('appends a generated class name for every referenced CSS module', () => { + let subject; - describe('options.allowMultiple', () => { - context('when multiple module names are used', () => { - context('when false', () => { - it('throws an error', () => { - expect(() => { - linkClass(
    , {}, {allowMultiple: false}); - }).to.throw(Error, 'ReactElement styleName property defines multiple module names ("foo bar").'); - }); - }); - context('when true', () => { - it('appends a generated class name for every referenced CSS module', () => { - let subject; + subject =
    ; - subject =
    ; + subject = linkClass(subject, { + bar: 'bar-1', + foo: 'foo-1' + }, { + allowMultiple: true + }); - subject = linkClass(subject, { - foo: 'foo-1', - bar: 'bar-1' - }, {allowMultiple: true}); + expect(subject.props.className).to.deep.equal('foo-1 bar-1'); + }); + }); + }); + }); + + describe('options.handleNotFoundStyleName', () => { + context('when styleName does not match an existing CSS module', () => { + context('when throw', () => { + it('throws an error', () => { + expect(() => { + linkClass(
    , {}, {handleNotFoundStyleName: 'throw'}); + }).to.throw(Error, '"foo" CSS module is undefined.'); + }); + }); + context('when log', () => { + it('logs a warning to the console', () => { + const warnSpy = chai.spy(() => {}); + + console.warn = warnSpy; + linkClass(
    , {}, {handleNotFoundStyleName: 'log'}); + expect(warnSpy).to.have.been.called(); + }); + }); + context('when ignore', () => { + it('does not log a warning', () => { + const warnSpy = chai.spy(() => {}); + + console.warn = warnSpy; + linkClass(
    , {}, {handleNotFoundStyleName: 'ignore'}); + expect(warnSpy).to.not.have.been.called(); + }); - expect(subject.props.className).to.deep.equal('foo-1 bar-1'); - }); - }); + it('does not throw an error', () => { + expect(() => { + linkClass(
    , {}, {handleNotFoundStyleName: 'ignore'}); + }).to.not.throw(Error, '"foo" CSS module is undefined.'); }); + }); }); + }); - describe('options.errorWhenNotFound', () => { - context('when styleName does not match an existing CSS module', () => { - context('when false', () => { - it('ignores the missing CSS module', () => { - let subject; + context('when ReactElement includes ReactComponent', () => { + let Foo; + let nodeList; - subject =
    ; + beforeEach(() => { + global.document = jsdom.jsdom(''); + global.window = document.defaultView; - subject = linkClass(subject, {}, {errorWhenNotFound: false}); + Foo = class extends React.Component { + render () { + return
    Hello
    ; + } + }; - expect(subject.props.className).to.be.an('undefined'); - }); - }); - context('when is true', () => { - it('throws an error', () => { - expect(() => { - linkClass(
    , {}, {errorWhenNotFound: true}); - }).to.throw(Error, '"foo" CSS module is undefined.'); - }); - }); - }); + nodeList = TestUtils.renderIntoDocument(linkClass(
    , {foo: 'foo-1'})); }); + it('processes ReactElement nodes', () => { + expect(nodeList.className).to.equal('foo-1'); + }); + it('does not process ReactComponent nodes', () => { + expect(nodeList.firstChild.className).to.equal(''); + }); + }); - context('when ReactElement includes ReactComponent', () => { - let Foo, - nodeList; + it('deletes styleName property from the target element', () => { + let subject; - beforeEach(() => { - global.document = jsdom.jsdom(''); - global.window = document.defaultView; + subject =
    ; - Foo = class extends React.Component { - render () { - return
    Hello
    ; - } - }; + subject = linkClass(subject, { + foo: 'foo-1' + }); - nodeList = TestUtils.renderIntoDocument(linkClass(
    , {foo: 'foo-1'})); - }); - it('processes ReactElement nodes', () => { - expect(nodeList.className).to.equal('foo-1'); - }); - it('does not process ReactComponent nodes', () => { - expect(nodeList.firstChild.className).to.equal(''); - }); + expect(subject.props.className).to.deep.equal('foo-1'); + expect(subject.props).not.to.have.property('styleName'); + }); + + it('deletes styleName property from the target element (deep)', () => { + let subject; + + subject =
    } + els={[, []]} + styleName='foo' + > +
    +
    +
    ; + + subject = linkClass(subject, { + bar: 'bar-1', + baz: 'baz-1', + foo: 'foo-1' }); + + expect(subject.props.children[0].props.className).to.deep.equal('bar-1'); + expect(subject.props.children[0].props).not.to.have.property('styleName'); + expect(subject.props.el.props.className).to.deep.equal('baz-1'); + expect(subject.props.el.props).not.to.have.property('styleName'); + expect(subject.props.els[0].props.className).to.deep.equal('foo-1'); + expect(subject.props.els[0].props).not.to.have.property('styleName'); + expect(subject.props.els[1][0].props.className).to.deep.equal('bar-1'); + expect(subject.props.els[1][0].props).not.to.have.property('styleName'); + }); }); diff --git a/tests/makeConfiguration.js b/tests/makeConfiguration.js index 3a74ae7..4193311 100644 --- a/tests/makeConfiguration.js +++ b/tests/makeConfiguration.js @@ -3,43 +3,40 @@ import { expect } from 'chai'; - import makeConfiguration from './../src/makeConfiguration'; describe('makeConfiguration', () => { - describe('when using default configuration', () => { - let configuration; + describe('when using default configuration', () => { + let configuration; - beforeEach(() => { - configuration = makeConfiguration(); - }); - describe('allowMultiple property', () => { - it('defaults to false', () => { - expect(configuration.allowMultiple).to.equal(false); - }); - }); - describe('errorWhenNotFound property', () => { - it('defaults to true', () => { - expect(configuration.errorWhenNotFound).to.equal(true); - }); - }); + beforeEach(() => { + configuration = makeConfiguration(); }); - describe('when unknown property is provided', () => { - it('throws an error', () => { - expect(() => { - makeConfiguration({ - unknownProperty: true - }); - }).to.throw(Error, 'Unknown configuration property "unknownProperty".'); + describe('allowMultiple property', () => { + it('defaults to false', () => { + expect(configuration.allowMultiple).to.equal(false); + }); + }); + describe('handleNotFoundStyleName property', () => { + it('defaults to "throw"', () => { + expect(configuration.handleNotFoundStyleName).to.equal('throw'); + }); + }); + }); + describe('when unknown property is provided', () => { + it('throws an error', () => { + expect(() => { + makeConfiguration({ + unknownProperty: true }); + }).to.throw(Error, 'Unknown configuration property "unknownProperty".'); }); - it('does not mutate user configuration', () => { - let userConfiguration; + }); + it('does not mutate user configuration', () => { + const userConfiguration = {}; - userConfiguration = {}; + makeConfiguration(userConfiguration); - makeConfiguration(userConfiguration); - - expect(userConfiguration).to.deep.equal({}); - }); + expect(userConfiguration).to.deep.equal({}); + }); }); diff --git a/tests/reactCssModules.js b/tests/reactCssModules.js index 3e911c8..d484d95 100644 --- a/tests/reactCssModules.js +++ b/tests/reactCssModules.js @@ -1,138 +1,184 @@ -/* eslint-disable max-nested-callbacks, react/no-multi-comp */ +/* eslint-disable max-nested-callbacks, react/no-multi-comp, react/prop-types, react/prefer-stateless-function, class-methods-use-this */ import { expect } from 'chai'; - import React from 'react'; +import ReactDOM from 'react-dom'; import TestUtils from 'react-addons-test-utils'; +import jsdom from 'jsdom'; import reactCssModules from './../src/index'; describe('reactCssModules', () => { - context('a ReactComponent is decorated using react-css-modules', () => { - it('inherits displayName', () => { - let Foo; + context('a ReactComponent is decorated using react-css-modules', () => { + it('inherits displayName', () => { + let Foo; - Foo = class extends React.Component {}; + Foo = class extends React.Component {}; - // @todo https://phabricator.babeljs.io/T2779 - Foo.displayName = 'Bar'; + // @todo https://phabricator.babeljs.io/T2779 + Foo.displayName = 'Bar'; - Foo = reactCssModules(Foo); + Foo = reactCssModules(Foo); - expect(Foo.displayName).to.equal('Bar'); - }); - context('target component does not name displayName', () => { - it('uses name for displayName', () => { - let Foo; + expect(Foo.displayName).to.equal('Bar'); + }); + context('target component does not name displayName', () => { + it('uses name for displayName', () => { + let Foo; - Foo = class Bar extends React.Component {}; + Foo = class Bar extends React.Component {}; - Foo = reactCssModules(Foo); + Foo = reactCssModules(Foo); - expect(Foo.displayName).to.equal('Bar'); - }); - }); + expect(Foo.displayName).to.equal('Bar'); + }); }); - context('a ReactComponent renders an element with the styleName prop', () => { - context('the component is a class that extends React.Component', () => { - it('that element should contain the equivalent className', () => { - let Foo, - component, - shallowRenderer; - - shallowRenderer = TestUtils.createRenderer(); + }); + context('a ReactComponent renders an element with the styleName prop', () => { + context('the component is a class that extends React.Component', () => { + let Foo; + let component; + + beforeEach(() => { + const shallowRenderer = TestUtils.createRenderer(); + + Foo = class extends React.Component { + render () { + return
    Hello
    ; + } + }; + + Foo = reactCssModules(Foo, { + foo: 'foo-1' + }); - Foo = class extends React.Component { - render () { - return
    Hello
    ; - } - }; + shallowRenderer.render(); - Foo = reactCssModules(Foo, { - foo: 'foo-1' - }); + component = shallowRenderer.getRenderOutput(); + }); + it('that element should contain the equivalent className', () => { + expect(component.props.className).to.equal('foo-1'); + }); + it('the styleName prop should be "consumed" in the process', () => { + expect(component.props).not.to.have.property('styleName'); + }); + }); + context('the component is a stateless function component', () => { + let Foo; + let component; - shallowRenderer.render(); + beforeEach(() => { + const shallowRenderer = TestUtils.createRenderer(); - component = shallowRenderer.getRenderOutput(); + Foo = () => { + return
    Hello
    ; + }; - expect(component.props.className).to.equal('foo-1'); - }); + Foo = reactCssModules(Foo, { + foo: 'foo-1' }); - context('the component is a stateless function component', () => { - it('that element should contain the equivalent className', () => { - let Foo, - component, - shallowRenderer; - shallowRenderer = TestUtils.createRenderer(); + shallowRenderer.render(); - Foo = () => { - return
    Hello
    ; - }; - - Foo = reactCssModules(Foo, { - foo: 'foo-1' - }); + component = shallowRenderer.getRenderOutput(); + }); + it('that element should contain the equivalent className', () => { + expect(component.props.className).to.equal('foo-1'); + }); + it('the styleName prop should be "consumed" in the process', () => { + expect(component.props).not.to.have.property('styleName'); + }); + }); + }); + context('a ReactComponent renders nothing', () => { + context('the component is a class that extends React.Component', () => { + it('linkClass must not intervene', () => { + let Foo; + + const shallowRenderer = TestUtils.createRenderer(); + + Foo = class extends React.Component { + render () { + return null; + } + }; + + Foo = reactCssModules(Foo, { + foo: 'foo-1' + }); - shallowRenderer.render(); + shallowRenderer.render(); - component = shallowRenderer.getRenderOutput(); + const component = shallowRenderer.getRenderOutput(); - expect(component.props.className).to.equal('foo-1'); - }); - }); + expect(typeof component).to.equal('object'); + }); }); + context('the component is a stateless function component', () => { + it('that element should contain the equivalent className', () => { + let Foo; - context('a ReactComponent renders nothing', () => { - context('the component is a class that extends React.Component', () => { - it('linkClass must not intervene', () => { - let Foo, - component, - shallowRenderer; + const shallowRenderer = TestUtils.createRenderer(); - shallowRenderer = TestUtils.createRenderer(); + Foo = () => { + return null; + }; - Foo = class extends React.Component { - render () { - return null; - } - }; + Foo = reactCssModules(Foo, { + foo: 'foo-1' + }); - Foo = reactCssModules(Foo, { - foo: 'foo-1' - }); + shallowRenderer.render(); - shallowRenderer.render(); + const component = shallowRenderer.getRenderOutput(); - component = shallowRenderer.getRenderOutput(); + expect(typeof component).to.equal('object'); + }); + }); + }); + context('rendering element', () => { + beforeEach(() => { + global.document = jsdom.jsdom(''); - expect(typeof component).to.equal('object'); - }); + global.window = document.defaultView; + }); + context('parent component is using react-css-modules and interpolates props.children', () => { + // @see https://github.com/gajus/react-css-modules/issues/76 + it('unsets the styleName property', () => { + let Bar; + let Foo; + let subject; + + Foo = class extends React.Component { + render () { + return +
    foo
    +
    ; + } + }; + + Foo = reactCssModules(Foo, { + test: 'foo-0' }); - context('the component is a stateless function component', () => { - it('that element should contain the equivalent className', () => { - let Foo, - component, - shallowRenderer; - - shallowRenderer = TestUtils.createRenderer(); - Foo = () => { - return null; - }; + Bar = class extends React.Component { + render () { + return
    {this.props.children}
    ; + } + }; - Foo = reactCssModules(Foo, { - foo: 'foo-1' - }); + Bar = reactCssModules(Bar, { + test: 'bar-0' + }); - shallowRenderer.render(); + subject = TestUtils.renderIntoDocument(); - component = shallowRenderer.getRenderOutput(); + // eslint-disable-next-line react/no-find-dom-node + subject = ReactDOM.findDOMNode(subject); - expect(typeof component).to.equal('object'); - }); - }); + expect(subject.firstChild.className).to.equal('foo-0'); + }); }); + }); }); diff --git a/tests/renderNothing.js b/tests/renderNothing.js new file mode 100644 index 0000000..7a666e8 --- /dev/null +++ b/tests/renderNothing.js @@ -0,0 +1,16 @@ +import { + expect +} from 'chai'; +import renderNothing from '../src/renderNothing'; + +describe('renderNothing', () => { + context('renderNothing should return different node types for various React versions', () => { + it('should return noscript tag for React v14 or lower', () => { + expect(renderNothing('14.0.0').type).to.equal('noscript'); + }); + + it('should return null for React v15 or higher', () => { + expect(renderNothing('15.0.0')).to.equal(null); + }); + }); +}); diff --git a/tests/wrapStatelessFunction.js b/tests/wrapStatelessFunction.js index f66c43e..9c71935 100644 --- a/tests/wrapStatelessFunction.js +++ b/tests/wrapStatelessFunction.js @@ -3,72 +3,90 @@ import { expect } from 'chai'; - +import React from 'react'; +import TestUtils from 'react-addons-test-utils'; import wrapStatelessFunction from './../src/wrapStatelessFunction'; describe('wrapStatelessFunction', () => { - it('hoists static own properties from the input component to the wrapped component', () => { - let Component, WrappedComponent, styles; + it('hoists static own properties from the input component to the wrapped component', () => { + const styles = { + foo: 'foo-1' + }; - styles = { - foo: 'foo-1' - }; + const InnerComponent = () => { + return null; + }; - Component = function InnerComponent () { - return null; - }; + InnerComponent.propTypes = {}; + InnerComponent.defaultProps = {}; - Component.propTypes = {}; - Component.defaultProps = {}; + const WrappedComponent = wrapStatelessFunction(InnerComponent, styles); - WrappedComponent = wrapStatelessFunction(Component, styles); + expect(WrappedComponent.propTypes).to.equal(InnerComponent.propTypes); + expect(WrappedComponent.defaultProps).to.equal(InnerComponent.defaultProps); + expect(WrappedComponent.name).not.to.equal(InnerComponent.name); + }); + context('using default styles', () => { + it('exposes styles through styles property', (done) => { + const styles = { + foo: 'foo-1' + }; - expect(WrappedComponent.propTypes).to.equal(Component.propTypes); - expect(WrappedComponent.defaultProps).to.equal(Component.defaultProps); - expect(WrappedComponent.name).not.to.equal(Component.name); + wrapStatelessFunction((props) => { + expect(props.styles).to.equal(styles); + done(); + }, styles)(); }); - context('using default styles', () => { - it('exposes styles through styles property', (done) => { - let styles; - - styles = { - foo: 'foo-1' - }; + it('exposes non-enumerable styles property', (done) => { + const styles = { + foo: 'foo-1' + }; - wrapStatelessFunction((props) => { - expect(props.styles).to.equal(styles); - done(); - }, styles)(); - }); - it('does not affect the other instance properties', (done) => { - let styles; + wrapStatelessFunction((props) => { + expect(props.propertyIsEnumerable('styles')).to.equal(false); + done(); + }, styles)(); + }); + it('does not affect the other instance properties', (done) => { + const styles = { + foo: 'foo-1' + }; - styles = { - foo: 'foo-1' - }; + wrapStatelessFunction((props) => { + expect(props.bar).to.equal('baz'); + done(); + }, styles)({ + bar: 'baz' + }); + }); + }); + context('using explicit styles', () => { + it('exposes styles through styles property', (done) => { + const styles = { + foo: 'foo-1' + }; - wrapStatelessFunction((props) => { - expect(props.bar).to.equal('baz'); - done(); - }, styles)({ - bar: 'baz' - }); - }); + wrapStatelessFunction((props) => { + expect(props.styles).to.equal(styles); + done(); + })({ + styles + }); }); - context('using explicit styles', () => { - it('exposes styles through styles property', (done) => { - let styles; + }); + context('rendering Component that returns null', () => { + it('generates null', () => { + const shallowRenderer = TestUtils.createRenderer(); + + const Component = wrapStatelessFunction(() => { + return null; + }); + + shallowRenderer.render(); - styles = { - foo: 'foo-1' - }; + const component = shallowRenderer.getRenderOutput(); - wrapStatelessFunction((props) => { - expect(props.styles).to.equal(styles); - done(); - })({ - styles - }); - }); + expect(component).to.equal(null); }); + }); });