Jest: Jest snapshot and CSS modules

Created on 1 Aug 2016  路  9Comments  路  Source: facebook/jest

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 !

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 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>
`;

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 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!

All 9 comments

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.

tl;dr

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>
`;

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 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 !

Was this page helpful?
0 / 5 - 0 ratings