OFFLlNE
OFFLlNE

Reputation: 797

How to mock history.listen?

history.js

import { createBrowserHistory } from 'history';

export default createBrowserHistory();

In my .js this is how I am using history.listen

import history from './history';

The following in the constructor:

history.listen(location => {
  if (location.pathname === '/second') {
    this.setState({
      pathStep: 1,
    });
  } else if (location.pathname === '/') {
    this.setState({
      pathStep: 0,
    });
  }
});

Now I am struggling to have a valid test for that:

    I tried to do the following:
    jest.mock('./history', () => ({
      listen: () => () => {
        '/second';
      },
    }));

    it('changes activeStep when called', () => {
      expect(component.state().pathStep).toBe(1);
    });

But even adding a console.log after history.listen(location => { I am not reaching my history.listen. So I am curious what I am doing wrong

I also tried adding spyOn to history.listen, but keen to hear what is the best practice for this specific test

Upvotes: 3

Views: 4545

Answers (2)

Brian Adams
Brian Adams

Reputation: 45820

If you mock history.listen, you can get the callback that your component passes to it.

Then, you can call the callback directly to verify that your component responds correctly.

Here is a complete working example:

history.js

import { createBrowserHistory } from 'history';

export default createBrowserHistory();

code.js

import * as React from 'react';
import history from './history';

export class SimpleComponent extends React.Component {
  constructor(...args) {
    super(...args);
    this.state = { pathStep: 0 };
  }
  componentDidMount() {
    this.unlisten = history.listen(location => {
      if (location.pathname === '/second') {
        this.setState({
          pathStep: 1,
        });
      } else if (location.pathname === '/') {
        this.setState({
          pathStep: 0,
        });
      }
    });
  }
  componentWillUnmount() {
    this.unlisten();
  }
  render() { return null; }
}

code.test.js

import * as React from 'react';
import history from './history';
import { mount } from 'enzyme';

import { SimpleComponent } from './code';

test('SimpleComponent', () => {
  const listenMock = jest.spyOn(history, 'listen');
  const unlistenMock = jest.fn();
  listenMock.mockReturnValue(unlistenMock);

  const component = mount(<SimpleComponent />);
  expect(component.state().pathStep).toBe(0);  // Success!

  const callback = listenMock.mock.calls[0][0];  // <= get the callback passed to history.listen

  callback({ pathname: '/second' });
  expect(component.state().pathStep).toBe(1);  // Success!

  callback({ pathname: '/' });
  expect(component.state().pathStep).toBe(0);  // Success!

  component.unmount();
  expect(unlistenMock).toHaveBeenCalled();  // Success!
})

Upvotes: 2

OFFLlNE
OFFLlNE

Reputation: 797

What I ended up doing was something like that. On the very first mount it returns /, on the second mount it returns /second and from there it is back to the default state of 0(/)

jest.mock('./history', () => ({
  listen: jest
    .fn()
   .mockImplementationOnce(cb => {
      cb({ pathname: '/' });
    })
    .mockImplementationOnce(cb => {
      cb({ pathname: '/second' });
    }),
}));

And the test itself (to test both /second and / in 1 single test)

it('changes pathStep when called', () => {
    expect(component.state().pathStep).toBe(0);

    component = mount(<MyComponent />);

    expect(component.state().pathStep).toBe(1);

    component = mount(<MyComponent />);

    expect(component.state().pathStep).toBe(0);
  });

But to make the test work that I asked initially something like that will suffice:

jest.mock('./history', () => ({
  listen: cb => {
    cb({ pathname: '/second' });
  },
}));

I just had to pass a callback when mocking, so close to what I had before, but with some pairing managed to get it to work :)

Hope that it makes sense and will help somebody out in the future

Upvotes: 1

Related Questions