Hi,
I'm trying to use the new Jest snapshot feature to write tests and while the tests passes, the output is not right.
It seems that classes imported from a CSS module and applied dynamically using classnames lib do not work at all.
If I print my component props after being rendered to JSON :
{ className: '' }
{ className: 'undefined' }
{ className: '' }
'undefined' should actually be two CSS classes and '' should also be a CSS class.
(The component works perfectly fine in the browser)
Any idea how I can make this work ?
Thanks !
Can you try out the identify-obj-proxy module as described here: http://facebook.github.io/jest/docs/tutorial-webpack.html#content ?
(note: requires node 6).
So I tried (with node 4 and the --harmony_proxies as per documentation) and it still give me the same results. Since I have automock set to false, should I explicitly say somewhere to mock the css files ?
Can you share a repository with your Jest config that shows this problem? Otherwise it will be impossible to help.
I can give you a stripped down version of the config I use, hope it helps :
package.json
"jest": {
"automock": false,
"moduleDirectories": [
"node_modules",
"src"
],
"testPathDirs": [
"src"
],
"moduleNameMapper": {
"^.*[.](css|CSS)$": "<rootDir>/test/styleMock.js",
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/test/fileMock.js"
}
}
test/styleMock.js
import idObj from 'identity-obj-proxy';
export default idObj;
test/fileMock.js
export default '';
webpack.config.js
const path = require('path');
const webpack = require('webpack');
const AssetsPlugin = require('assets-webpack-plugin');
const assetsPluginInstance = new AssetsPlugin();
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const autoPrefixer = require('autoprefixer');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const baseConf = {
entry: {
main: './src/client/index',
},
devtool: 'source-map',
output: {
path: path.join(__dirname, './public/dist'),
publicPath: '/assets/dist/',
filename: '[name].[hash].js',
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loaders: ['babel-loader'],
},
{
test: /\.css$/,
loader: ExtractTextPlugin.extract('style-loader',
'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]!postcss-loader'),
},
{
test: /\.json$/,
loaders: ['json-loader'],
},
],
},
postcss: [
autoPrefixer,
],
plugins: [
new CleanWebpackPlugin(['public/dist'], {
verbose: true,
}),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
},
}),
new webpack.optimize.CommonsChunkPlugin({
minChunks: 2,
filename: 'common.[hash].js',
name: 'common',
}),
new ExtractTextPlugin('[name].[hash].css'),
assetsPluginInstance,
],
resolve: {
modulesDirectories: ['src', 'node_modules'],
extensions: ['', '.json', '.js'],
},
node: {
__dirname: true,
fs: 'empty',
},
};
module.exports = baseConf;
animated-input.js
import React, { PropTypes } from 'react';
import _ from 'lodash';
import classNames from 'classnames';
import block from 'shared/higher/block';
import { onFocus } from 'shared/higher/events';
import styles from './animated-input.css';
class AnimatedInput extends React.PureComponent {
static propTypes = {
label: PropTypes.string.isRequired,
};
componentWillMount() {
this.id = _.uniqueId('input_');
}
render() {
const {
label,
focus,
...other,
} = this.props;
const animatedInputClassNames = classNames({
[`${styles.animatedInputWrapper}`]: true,
[`${styles.active}`]: focus,
});
return (
<div className={animatedInputClassNames}>
<input
{...other}
id={this.id}
className={styles.input}
/>
<label className={styles.label} htmlFor={this.id}>{label}</label>
</div>
);
}
}
export default onFocus(block(AnimatedInput));
蹋__tests__/animated-input.test.js
import React from 'react';
import AnimatedInput from '../index';
import renderer from 'react-test-renderer';
describe('AnimatedInput', () => {
it('renders correctly', () => {
const tree = renderer.create(
<AnimatedInput label="Username" />
).toJSON();
expect(tree).toMatchSnapshot();
});
it('changes the class when focused', () => {
const component = renderer.create(
<AnimatedInput label="Username" />
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
console.log(tree.props);
// manually trigger the callback
tree.children[0].props.onFocus();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
console.log(tree.props);
// manually trigger the callback
tree.children[0].props.onBlur();
// re-rendering
tree = component.toJSON();
console.log(tree.props);
expect(tree).toMatchSnapshot();
});
});
cc @keyanzhang can you take a look at this? I thought that this would work exactly as expected with your object proxy module.
Sure, let me see if I can repro this.
Hey @moimael,
Thanks for reporting this! I have to say this is the most interesting bug I've fixed in a while haha.
I published a new version of identity-obj-proxy
. Please upgrade it to 3.0.0
and it should work as expected. I also made a simple example which can be found at https://github.com/keyanzhang/jest-css-modules-example/.
For a component like
import React, { Component } from 'react';
import styles from './App.css';
export default class App extends Component {
render() {
return (
<div className={styles.root}>
<h1 className={styles.hello}>Hello, world!</h1>
</div>
);
}
}
it generates a snapshot as below:
exports[`test App renders correctly 1`] = `
<div
className="root">
<h1
className="hello">
Hello, world!
</h1>
</div>
`;
When we use an ES6 default import (like import foo from './foo';
), it looks for the __esModule
field to determine whether the right hand side is an ES6 export
. If __esModule
is falsy then it fallbacks to behave like an ES5-style require
instead. The tricky part here is: since idObj
just returns whatever the key is, the value of idObj.__esModule
is just '__esModule'
as a string. Thus '__esModule'
becomes the value of idObj
and '__esModule'.whatever
is just undefined
. Isn't it fun? :wink:
The fix can be found here. Now as long as people don't use __esModule
as a class name it should work fine!
This is so awesome. Thanks @keyanzhang for finding the issue!
@keyanzhang Awesome catch ! It works like a charm with the new release. Thanks everyone for your awesome work !
Most helpful comment
Hey @moimael,
Thanks for reporting this! I have to say this is the most interesting bug I've fixed in a while haha.
tl;dr
I published a new version of
identity-obj-proxy
. Please upgrade it to3.0.0
and it should work as expected. I also made a simple example which can be found at https://github.com/keyanzhang/jest-css-modules-example/.For a component like
it generates a snapshot as below:
What happened?
When we use an ES6 default import (like
import foo from './foo';
), it looks for the__esModule
field to determine whether the right hand side is an ES6export
. If__esModule
is falsy then it fallbacks to behave like an ES5-stylerequire
instead. The tricky part here is: sinceidObj
just returns whatever the key is, the value ofidObj.__esModule
is just'__esModule'
as a string. Thus'__esModule'
becomes the value ofidObj
and'__esModule'.whatever
is justundefined
. Isn't it fun? :wink:The fix can be found here. Now as long as people don't use
__esModule
as a class name it should work fine!