Skip to content
This repository has been archived by the owner on Sep 10, 2022. It is now read-only.

using recompose with enzyme #407

Closed
komkanit opened this issue Jun 16, 2017 · 12 comments
Closed

using recompose with enzyme #407

komkanit opened this issue Jun 16, 2017 · 12 comments

Comments

@komkanit
Copy link

My enhance it's look like this

const enhance = compose(
  withState('labelText', 'setLabelText', ''),
  withState('hover', 'hovering', false),
  withState('labelLeft', 'setLabelLeftPosition', 0),
);

When I use enyzme to test component and I console.log the props like this

const wrapper = shallow(<Toolbox />);
console.log(wrapper.props());

It's show only first props { labelText: '', setLabelText: [Function] }
Did you know what is happening?

@istarkov
Copy link
Contributor

Yes I know. Please read about what is higher order components.

@Isaddo
Copy link

Isaddo commented Jun 16, 2017

use mount instead of shallow

@komkanit
Copy link
Author

@Isaddo When I change shallow to mount I got output {} from console

@istarkov
Copy link
Contributor

Just think about this

  const C = ({ propA, propB }) => <div>{propA + propB}</div>
  const B = ({ propA }) => <C propB={'propB'} propA={propA} />
  const A = () => <B propA={'propA'} />
  const wrapper = shallow(<A />)

It is almost the same what you have with enhancers. Every enhancer here - HOC creates a new Component so at top level component you have props only from top enhancer

@istarkov
Copy link
Contributor

To test all the props you need to use something like

  const ToolboxClass = toClass(Toolbox) // otherwise won't work with stateless

  const Blabla = compose(
    withState('labelText', 'setLabelText', ''),
    withState('hover', 'hovering', false),
    withState('labelLeft', 'setLabelLeftPosition', 0)
  )(ToolboxClass)

  const wrapper = mount(<Blabla />)

  console.log(wrapper.find(ToolboxClass).props())

@komkanit
Copy link
Author

I did it! Thank you so much @istarkov

@komkanit
Copy link
Author

@istarkov Have anyway to access Base Component if I import a Composed Component?

@istarkov
Copy link
Contributor

I always export base component, enhancer and enhanced component,
it allows me to reuse them, and also to use code above for tests

@marcusstenbeck
Copy link

Just for completeness this is what worked for me.

// Toolbox.js

export const enhance = compose(
  withState('labelText', 'setLabelText', ''),
  withState('hover', 'hovering', false),
  withState('labelLeft', 'setLabelLeftPosition', 0)
);

export Toolbox = /* ... */

export default enhance(Toolbox);


// Toolbox.test.js

import { Toolbox as ToolboxComponent, enhance } from './Toolbox';

const ToolboxClass = toClass(ToolboxComponent); // otherwise won't work with stateless
const Toolbox = enhance(ToolboxClass);

const wrapper = mount(<Toolbox />);

console.log(wrapper.find(ToolboxClass).props());

@fakiolinho
Copy link

fakiolinho commented Oct 20, 2017

@marcusstenbeck this is really helpful. I 've searched a while to find this. I have one question if you don't mind. What if I want to pass some dummy props in my tests. For example let's say i have prop A and B and I use a boolean prop C injected by withProps after making some calculations with A and B.

export const enhance = compose(
  pure,
  withProps(({ A, B }) => ({
    C: ...
  })),
  withHandlers({
    onClick: () => e => (
    ...
    ),
  }),
);

export const Component = ({
  A,
  B,
  C,
  onClick,
}) => (
  <div onClick={onClick}>
    <p>{C ? 'C is truthy' : 'C is falsy'}</p>
  </div>
);

Component.propTypes = {
  A: PropTypes.arrayOf(PropTypes.string),
  B: PropTypes.string,
  C: PropTypes.bool,
  onClick: PropTypes.func,
};

export default enhance(Component);

Here without recompose i 'd use some dummy A, B and then i would test the final outcome accordingly. Now i am creating C with recompose (by using A and B) and the outcome is based on C. So now that C is also a prop how should i test this component? How can i inject these props in your example above?

@timkindberg
Copy link
Contributor

I prefer to test my smart recomposed component. Because often, there is logic there that needs testing. Exporting the dumb component and testing that seems like a shortcut to me.

I use this until helper to easily "dive" past all the HOCs to the Base Component that I'm ultimately wrapping.

So if you have a src file of:

// ToolBox.js
const enhance = compose(
  withState('labelText', 'setLabelText', 'my label'),
  withState('hover', 'hovering', false),
  withState('labelLeft', 'setLabelLeftPosition', 0),
);

export const ToolBox = enhance(
  ({ labelText, hover, labelLeft, ...etc }) =>
    <div>{ labelText }{ hover }{ labelLeft }</div>
);

You can have a test file of (I'm writing an example here, not sure if this is working, but this is what my tests basically look like):

// ToolBox.spec.js
it('has the props', () => {
  // shallow the entire component, recompose and everything.
  // I do still use mount sometimes for testing lifecycles or other reasons, but rarely.
  // I'm using the ::until syntax (it is stage-0) but you can also add to ShallowWrapper.prototype
  // ::until('ToolBox') will continue to shallow render single children until it finds the matcher
  const wrapper = shallow(<ToolBox />)::until('ToolBox');

  expect(wrapper.text()).toEqual('my label false 0')
});

You have to mix that approach with solid HOC mocks for common HOCs like connect from react-redux. Here is my mock for connect, it's the most basic version of connect you could write, so it still maps state and dispatch to props but in a very basic way that means you don't have to worry about Provider errors. I also typically just mock selectors but sometimes I'll use the setTestState helper to mock out the redux state (though I'm using that less and less).

// __mocks__/react-redux.js

import React from 'react';

let state = { };

const setTestState = (newState) => { state = newState; };

const connect =
  (mapStateToProps, mapDispatchToProps) => (BaseComponent) => {
    return (props) => {
      const mappedState = typeof mapStateToProps === 'function'
        ? mapStateToProps(state, props)
        : {};

      const mappedDispatchToProps = typeof mapDispatchToProps === 'function'
        ? mapDispatchToProps((action) => action, props)
        : mapDispatchToProps;

      return (
        <BaseComponent
          {...props}
          {...mappedState}
          {...mappedDispatchToProps}
        />
      );
    };
  };

module.exports = {
  setTestState,
  connect
};

@danielkrich
Copy link

danielkrich commented Jan 24, 2018

@timkindberg this looked promising, and I tried to use the module https://github.com/Stupidism/enzyme-shallow-until
But I still can't figure out what's wrong. I'm trying to test a component that uses a "componentDidMount" lifecycle. Basically I expect the base-component's props to change before and after this lifecycle.

This is the enhancer I try to test:

export const withInitialData = ({task, initialDataName, setData = 'setInitialData'}) => compose(
  withState(initialDataName, setData),
  lifecycle({
    state: { loading: true, error: null },
    componentDidMount() {
      runIfExists(task, this.props)
        .fork(error => {
            console.error(error)
            this.setState({ loading: false, error: error && error.message })
          },
          (data) => {
            this.props[setData](data)
            this.setState({ loading: false })
          })
    }
  })
)

And here is the test:

test('withInitialData should load successful data', async () => {
  const timeout = 10
  const dataName = "test"
  const data = "OK"
  const successfulTask = new Task((rej, res) => setTimeout(() => res(data), timeout)) //this is a dummy task I give to my recompose enhancer, when the tasks ends, it puts "loading: false" and the data into the baseComponent's props.
  const EnhancedInitialData = withInitialData({
    task: successfulTask,
    initialDataName: dataName
  })(MockComponent)
  const mountedInitialData = mount(<EnhancedInitialData/>)

  const props = getProps(mountedInitialData) //getProps is a function: `wrapper.find(component || MockComponent).props()`
  expect(props.loading).toBe(true) //this is true
  expect(props.error).toBe(null) //this is also true, equal to the the initial state

  await new Promise(resolve => setTimeout(resolve, 2000)) // this is to make sure the dummy task has finished and the component mounted successfuly
  // mountedInitialData.instance().forceUpdate() // I tried updating the wrapper, didn't work
  // mountedInitialData.update()

  expect(props.loading).toBe(false) // loading is still true, so the test fails
  expect(props[dataName]).toBe(data) // data is undefined
})

the props are always:
{ test: undefined, setInitialData: [Function], loading: true, error: null }

Any ideas?


UPDATE:

I figured it out!
turned out you have to use .update() AND .find() the baseComponent again after the lifecycle.

so finally, my test looks like this:

const MockComponent = ({loading, data}) => <div>{loading}</div>

const getProps = (wrapper, component) =>
  wrapper.find(component || MockComponent).props()

test('withInitialData should load successful data', async () => {
  const timeout = 10
  const dataName = "test"
  const data = "OK"
  const successfulTask = new Task((rej, res) => setTimeout(() => res(data), timeout))
  const EnhancedInitialData = withInitialData({
    task: successfulTask,
    initialDataName: dataName,
  })(MockComponent)
  const mountedInitialData = mount(<EnhancedInitialData/>)

  const props = getProps(mountedInitialData)

  expect(props.loading).toBe(true)
  expect(props.error).toBe(null)

  await new Promise(resolve => setTimeout(resolve, timeout))

  mountedInitialData.update()

  const newProps = getProps(mountedInitialData)

  expect(newProps.loading).toBe(false)
  expect(newProps[dataName]).toBe(data)

})

Hope this comes in handy for whoever's struggling with these kind of tests :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants