Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stubbing an instance method requires component and wrapper updates #586

Closed
steakchaser opened this issue Sep 8, 2016 · 10 comments
Closed

Comments

@steakchaser
Copy link

steakchaser commented Sep 8, 2016

Hi. I have a simple presentation component who's instance method (i.e. handler) I'm trying to assert is actually called.

I'm grabbing an instance of the component and then stubbing the handler via sinon. I was expecting to then be able to call update on the wrapper, simulate an action, and then assert that the stub was actually called.

However, the stub never gets called...unless I also call forceUpdate on the component instance. Wondering if this is an issue with update or is expected behavior?

EDIT / Note: handleSubmit is being passed in as a prop which then takes as an argument the instance method handleSubmit because this is a redux-form.

Here's the example presentation component:

export class MyForm extends Component {

  constructor(props) {
    super(props)
  }

  render() {
    let { handleSubmit, submitting, fields: { user } } = this.props

    return (
      <form onSubmit={handleSubmit(this.handleSubmit)}>
        <div className="mbl">
          <Row>
            <Col sm={6}>
              <Input
                {...user.firstName}
                label="First Name"
                type="text"
                error={user.firstName.touched ? user.firstName.error : null}/>
            </Col>
            <Col sm={6}>
              <Input
                {...user.lastName}
                label="Last Name"
                type="text"
                error={user.lastName.touched ? user.lastName.error : null}/>
            </Col>
          </Row>
        </div>
        <div className="text-center">
          <InfoButton className="mbl" submitting={submitting} type="submit">Save</InfoButton>
        </div>
      </form>
    )
  }

  // This is the instance method to be stubbed
  handleSubmit(values, dispatch) {
    console.log("instance handleSubmit")
  }

}

Here's the example test:

describe('<MyForm />', function() {

  context('when submitted', function() {

    it('calls the components internal handleSubmit method', function() {

      const props = {
        fields: {
          user: {
            firstName: {},
            lastName: {},
          }
        },
        handleSubmit: fn => fn
      }

      const wrapper = shallow(<MyForm {...props}/>)

      // Stub the handleSubmit method
      const component = wrapper.instance()
      let handleSubmitStub = sinon.stub(component, 'handleSubmit', () => { })

      // Force the component and wrapper to update so that the stub is used
      // ONLY works when both of these are present
      component.forceUpdate()
      wrapper.update()

      // Submit the form
      wrapper.find('form').simulate('submit')

      expect(handleSubmitStub.callCount).to.equal(1)
    })
  })
})
@nrempel
Copy link

nrempel commented Oct 24, 2016

I'm also seeing this - thanks for the workaround.

Possibly related to #622?

@dimitridewit
Copy link

Any updates on this?

@ljharb
Copy link
Member

ljharb commented Feb 7, 2017

I wouldn't expect the wrapper to rerender unless props or state had changed.

Your best bet is to stub the method on MyForm.prototype before shallow-rendering, rather than waiting till you have the instance.

@iwllyu
Copy link

iwllyu commented Aug 14, 2017

#944

seems to come down differences between shallow and mount update implementations

@palaniichukdmytro
Copy link

It does not work for me. In my case onSubmit undefined, even when I passed the handleSubmit to props.
Any idea how to simulate click with onSubmit={handleSubmit(this.update)

export class EditDevice extends Component {
    update = device => {
        console.log(device, 'device')
        if (device.miConfiguration)
            device.miConfiguration.isMiEnabled = device.miConfiguration.miConfigurationType !== MiConfigurationTypes.AccessPointOnly

        this.props.update(device).then(({success, ...error}) => {
            if (!success)
                throw new SubmissionError(error)

            this.returnToList()
        })}

    returnToList = () => this.props.history.push({pathname: '/setup/devices', state: {initialSkip: this.props.skip}})

    render = () => {
        let {isLoadingInProgress, handleSubmit, initialValues: {deviceType} = {}, change} = this.props

        const actions = [
            <Button
                name='cancel'
                onClick={this.returnToList}
            >
                <FormattedMessage id='common.cancel' />
            </Button>,
            <Button
                name='save'
                onClick={handleSubmit(this.update)}
                color='primary'
                style={{marginLeft: 20}}
            >
                <FormattedMessage id='common.save' />
            </Button>]

        return (
            <Page onSubmit={handleSubmit(this.update)} title={<FormattedMessage id='devices.deviceInfo' />} actions={actions} footer={actions}>
                <form >
                    {isLoadingInProgress && <LinearProgress mode='indeterminate'/>}
                    <div style={pageStyles.gridWrapper}>
                        <Grid container>
                            <Grid item xs={12}>
                                <Typography type='subheading'>
                                    <FormattedMessage id='devices.overview' />
                                </Typography>
                            </Grid>
                </form>
            </Page>
        )
    }
}
describe.only('EditPage', () => {

    let page, submitting, touched, error, reset, onSave, onSaveResponse, push
    let device = {miConfiguration:{isMiEnabled: 'dimon', miConfigurationType: 'Type'}}
    let update = sinon.stub().resolves({success: true})
    let handleSubmit = fn => fn(device)

    beforeEach(() => {
        submitting = false
        touched = false
        error = null
        reset = sinon.spy()
    })
    const props = {
        initialValues:{
            ...device,
        },
        update,
        handleSubmit,
        submitting: submitting,
        deviceId:999,
        history:{push: push = sinon.spy()},
        skip: skip,
        reset,
    }

    page = shallow(<EditDevice {...props}/>)

    it('should call push back to list on successful response', async () => {
        let update = sinon.stub().resolves({success: true})
        let device = {miConfiguration:{isMiEnabled: 'dimon', miConfigurationType: 'Type'}}

        page.find(Field).findWhere(x => x.props().name === 'name').simulate('change', {}, 'good name')
        await page.find(Page).props().footer.find(x => x.props.name === saveButtonName).props.onClick()
        push.calledOnce.should.be.true
        push.calledWith({pathname: '/setup/devices', state: {initialSkip: skip}}).should.be.true
    })
})

@ljharb
Copy link
Member

ljharb commented Nov 24, 2017

@palaniichukdmytro that's because you're putting arrow functions in class properties; that makes them slower and harder to test. All of those should be instance methods, and you should this.foo = this.foo.bind(this) in the constructor. That will address your problem.

@abdennour
Copy link

No worries guys! You could mock anything :

and no need for forceUpdate. But directly, you can call the lifecycle method just after stubbing the arrow function :

wrapper.instance().componentDidMount()

So it can be:


      const wrapper = mount(<MyForm {...props}/>)

      // Stub the handleSubmit method
      const component = wrapper.instance()
      let handleSubmitStub = sinon.stub(component, 'handleSubmit', () => { })
     component.componentDidMount();


Enjoy!

@antgonzales
Copy link

@abdennour to be clear, your method works on mount and not shallow.

@noahehall
Copy link

noahehall commented Feb 6, 2018

@antgonzales @abdennour method does work with shallow, you just have to call render() instead of componentDidMount()

@ljharb
Copy link
Member

ljharb commented Jun 26, 2018

This seems resolved; happy to reopen if not.

@ljharb ljharb closed this as completed Jun 26, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants