Testing React withRouter

One area I’ve always found tricky when writing tests for React is where Higher Order Components are involved. I’ve found this complicates the test setup process. There are ways around this, which may or may not be possible depending on many factors. Sometimes you work on third party code that won’t accept ‘dramatic’ refactors just to scratch your own testing itches.

One example of where this problem might occur (particularly if you don’t read the docs!!) is with React Router, specifically when using withRouter.

const MyComponent = ({ history }) => { ... });

export default withRouter(MyComponent);

In this example I have MyComponent which wants to history.push('/some/location') when the user completes some action.

I’d like to test that this process takes place as expected.

Here was my first attempt. This way works, but there are some drawbacks:

import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';

// ...

  it('should redirect when on the happy path', () => {
    const history = createMemoryHistory({
      initialEntries: ['/starting/point']
    });

     const { getByLabelText } = render(
      <Router history={history}>
        <Provider>
          <MyComponent />
        </Provider>
      </Router>
    );

    expect(history.location.pathname).toEqual('/starting/point');

    const myInput = getByLabelText('Some label text');

    const value = { target: { value: 'a value here' } };

    fireEvent.change(myInput, value);

    fireEvent.keyPress(myInput, { key: 'Enter', code: 13, charCode: 13 });

    expect(history.location.pathname).toEqual(
      '/path/when/redirected'
    );
  });

This works.

There are a couple of drawbacks to this:

  • It involves a couple of extra imports.
  • Behind the scenes, withRouter is still used (afaik) but by wrapping in another Router, we can override the history prop.

As this particular approach is so common, React Router gives us an alternative / preferable way to test this workflowWrappedComponent.

  it('should redirect when on the happy path', () => {
    const history = { push: jest.fn() };

     const { getByLabelText } = render(
      <Provider>
        <MyComponent.WrappedComponent history={history} />
      </Provider>
    );

    const myInput = getByLabelText('Some label text');

    const value = { target: { value: 'a value here' } };

    fireEvent.change(myInput, value);

    fireEvent.keyPress(myInput, { key: 'Enter', code: 13, charCode: 13 });

    expect(history.push).toHaveBeenCalledWith(
      '/path/when/redirected'
    );
    expect(history.push).toHaveBeenCalledTimes(1);
  });

The necessary actions to ‘run’ this test are unchanged. However, there are fewer lines as we can make use of the existing constructs provided by React Router to aid our testing workflow.

This may have been extremely obvious to you.

I can’t remember if WrappedComponent has always been available and I have overlooked it, or it is something new since I last had to do testing with a project using React Router.

Either way, it’s time I refreshed my knowledge of the documentation. And hopefully this helps someone else when testing withRouter at some point in the future.

Using ReactCSSTransitionGroup with TypeScript

I wanted to use ReactCSSTransitionGroup to add a little animation to a React and TypeScript project I was working on.

Following the docs:

import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; // ES6

Seems ok. But on doing this, in my .tsx  file, I ended up with a compilation error:

Failed to compile.

/path/to/my/src/Home/Home.tsx
(2,8): Module '"/path/to/my/node_modules/@types/react-addons-css-transition-group/index"' has no default export.

The fix to this came to me, as I’d read this somewhere previously a few months ago when doing my original exploration of TypeScript.

The syntax I really needed was:

import * as ReactCSSTransitionGroup from 'react-addons-css-transition-group';

I honestly can’t remember where I first read this. But hopefully I can at least pass a helpful little tip on to you.

Node JS nginx & pm2 config

This is for personal reference and I make no guarantees any of this will work for you. I no longer need it, but don’t want to lose this either. Future reference, and all that.

upstream pm2_nodejs_upstream {
    server node:4000;
    keepalive 64;
}

server {
    listen 80;
    server_name api.example.com;
    root /data;

    # location / {
    #     return 200 "Hello from Example.com";
    # }
    
    location / {
    	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    	proxy_set_header Host $http_host;
    	proxy_set_header X-NginX-Proxy true;
    	proxy_http_version 1.1;
    	proxy_set_header Upgrade $http_upgrade;
    	proxy_set_header Connection "upgrade";
    	proxy_max_temp_file_size 0;
    	proxy_pass http://pm2_nodejs_upstream/;
    	proxy_redirect off;
    	proxy_read_timeout 240s;
    }

    error_log /var/log/nginx/api.example.com_error.log;
    access_log /var/log/nginx/api.example.com_access.log;
}

 

How I Fixed: UglifyJs Unexpected token: name (DropIn)

I’ve been trying – in vain – to build the front end for CodeReviewVideos. The issue I have been hitting upon is as follows:

ERROR in app.a2e9a6b7afa471d94d2b.js from UglifyJs
Unexpected token: name (DropIn) [./node_modules/braintree-web-drop-in-react/dist/index.js:109,0][app.a2e9a6b7afa471d94d2b.js:116396,6]

uglifyjs-unexpected-token-name-dropin

As the error states, the issue is with the UglifyJs plugin, which I use in combination with WebPack.

This is a frustrating show-stopping problem. Unless fixed, I couldn’t complete a build.

Here’s what I had in my WebPack prod config:

      new webpack.optimize.UglifyJsPlugin({
        beautify: false,
        mangle: {
          screw_ie8: true,
          keep_fnames: true
        },
        compress: {
          screw_ie8: true,
          warnings: false
        },
        comments: false
      }),

This was only in my WebPack prod config. Therefore I didn’t notice any issue until trying to build for prod.

Now, in truth, I didn’t write the code / config above. I copy / pasted from somewhere else (I forget where) and as it just worked I didn’t pay much attention to it.

When it stopped working, I got sad, then got on to trying to fix it.

My Solution

There’s a bunch of suggested general solutions to this problem. A quick Google will turn up plenty of GitHub issues. Unfortunately none of them were specific to my exact error.

In my case, as best I understand it, the Braintree Web Drop In React should have compiled the dist.js file down to ES5, but is instead, in ES6. I concluded this based on this and this.

dammit-jim-im-a-coder-not-a-webpack-genius
Dammit Jim, I’m a coder not a WebPack genius

Of course, I may be wrong.

Fixing this wasn’t that hard. I just needed to read the docs.

yarn add --dev uglifyjs-webpack-plugin

For me, this added:

"uglifyjs-webpack-plugin": "^1.2.4",

To my devDependencies in package.json.

After which I updated my prod.js WebPack config as follows:

"use strict";

const webpack = require("webpack");
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = function() {
  console.log("BUILDING PRODUCTION");

  return webpackMerge(commonConfig(), {

    plugins: [

      new UglifyJsPlugin({
        test: /\.js($|\?)/i,
        sourceMap: true,
        uglifyOptions: {
          mangle: {
            keep_fnames: true,
          },
          compress: {
            warnings: false,
          },
          output: {
            beautify: false,
          },
        },
      }),

  });
};

I’ve removed everything unrelated to this specific problem.

After which I could get my build to work.