enzymejs / enzyme

JavaScript Testing utilities for React
https://enzymejs.github.io/enzyme/
MIT License
19.95k stars 2.01k forks source link

simulated click on submit button in form does not submit form #308

Closed codekirei closed 6 years ago

codekirei commented 8 years ago

Say I have a simple form to test:

<form onSubmit={onSubmit} >
  <input />
  <button type="submit">Submit</button>
</form>

If I mount this component with enzyme, I can confirm onSubmit is called by simulating a submit event on the form element. However, if I simulate a click on the submit button, the form is not submitted and onSubmit is not called.

Is this working as intended? Seems inconsistent, but I could definitely be missing something.

Example repo here. In particular, compare the test cases.

leopoldjoy commented 8 years ago

@codekirei Yeah, I'm having the same issue. Is this behaviour expected?

prokaktus commented 8 years ago

Especially concerned, that this is mount and should, by design, simulate real DOM behaviour. Or maybe DOM simulation through mount has some limitation -- and in this case it should be explicitly specified.

colinramsay commented 8 years ago

https://github.com/airbnb/enzyme/blob/master/docs/future.md

"Event propagation is not supported". I assume that's what causes this problem.

blainekasten commented 8 years ago

I agree with @colinramsay. If any of you guys want to put up a PR to integrate Event Propagation, i'm sure the maintainers would greatly appreciate it.

BLamy commented 8 years ago

This is pretty rough. So if I'm getting this straight there is currently no way to test code which looks like this?

const LoginComponent = ({login}) => (
    <form
      onSubmit={e => {
        const username = e.target.children[0].value;
        const password = e.target.children[1].value;
        login(username, password);
      }}
    >
      <input type="text" placeholder="Username" />
      <input type="text" placeholder="Password" />
      <button type="submit">Sign In</button>
    </form>
);
ljharb commented 8 years ago

@BLamy there are tons of ways to test it:

What further testing is needed? In no way should you be trying to write tests that test React's "onSubmit" functionality, or of browsers' submit behavior - that's the job of the React team, or the browser implementors, respectively.

BLamy commented 8 years ago

@ljharb I was talking about no way of testing it using simulate.

If you .simulate('click') it doesn't work. If you .simulate('submit') then e is not bound to the right context and therefore doesn't have a .target property.

So yeah I can stub e and directly call onSubmit with a spy on login but realistically what I wanted to test was to make sure I was referencing my input fields right.

ljharb commented 8 years ago

@BLamy does wrapper.find('button').simulate('submit', { target: wrapper.find('button').get(0) }) work?

BLamy commented 8 years ago

Best I could do:

    const form = component.find('form').at(0),
    const children = form.render().children().children();
    form.simulate('submit', { target: { children } });

Which works but leaves this in the console

Warning: ReactComponentTreeDevtool: Missing React element for debugID 5 when building stack
Warning: ReactComponentTreeDevtool: Missing React element for debugID 5 when building stack
Warning: ReactComponentTreeDevtool: Missing React element for debugID 5 when building stack
Warning: ReactComponentTreeDevtool: Missing React element for debugID 5 when building stack

I'm using tape for my test

fernandopasik commented 8 years ago

For me the solution was to get the DOM element and trigger the click without using simulate.

import { mount, shallow } from 'enzyme';
import Button from '../button/button.js';
import Field from './field.js';
import Form from './form.js';
import React from 'react';
import { spy } from 'sinon';

describe('Form', () => {
  it('submit event when click submit', () => {
    const callback = spy();
    const wrapper = mount(
    <Form onSubmit={ callback }>
        <Field id="firstName" name="firstName" />
        <Field id="lastName" name="lastName" />
        <Field id="email" name="email" type="email" />
        <footer>
          <Button caption="Send" display="primary" type="submit" />
          <Button caption="Clear" type="reset" />
        </footer>
      </Form>
    );
    wrapper.find('[type="submit"]').get(0).click();
    expect(callback).to.have.been.called();
  });
});
ZephD commented 7 years ago

@fernandopasik "wrapper.find(...).get(...).click is not a function" (This may be due to shallow instead of mount...)

fernandopasik commented 7 years ago

@ZephD you can't call .get() with shallow rendering.

ljharb commented 7 years ago

Better would be expect(callback).to.have.property('callCount', 1) or something similar; noop getter assertions are very dangerous (because expect(callback).to.have.been.yogurt will silently pass, and is always a bug)

netrocc commented 7 years ago

The form submits if you use the submit event on the button.

const onSubmit = sinon.spy();
const wrapper = mount(
    <form onSubmit={onSubmit} />
);
const button = wrapper.find('button');
button.simulate('submit');
vmasto commented 7 years ago

@netrocc's solution seems to work, however I'm not sure why buttons are able to receive submit events. Is this something we can rely upon?

sohailykhan94 commented 7 years ago

Hey guys, I was able to get around this by doing:

component.find('form')
      .simulate('submit', { preventDefault () {} });

Looks hacky, but works.

DZuz14 commented 7 years ago

I used @sohailykhan94's solution to test if a form submittal was properly executing a handler function. Here's what it looks like, and it seems to work well with my Jest/Enzyme testing environment.

it('executes a handler function on submittal', () => {
    const form = cmp().find('form')

    expect(cmp().state().submitted).toEqual(false)
    form.simulate('submit', { preventDefault () {} })
    expect(cmp().state().submitted).toEqual(true)
})

Here is my example Form React Component. Of course this component is going to have more features, but I wanted to test if this worked, before building out the rest of it.

import React, { Component } from 'react'

export default class SimpleForm extends Component {
  constructor(props) {
    super(props)

    this.state = {
      firstName: '',
      email: '',
      submitted: false
    }
  }

  submitForm = () => {
    this.setState({ submitted: true })
  }

  render() {
    return (
      <form className="simple-form" onSubmit={this.submitForm}>

        <input name="firstName" />
        <input name="email" />

        <button type="submit">Submit</button>
      </form>
    )
  }
}
jamesseanwright commented 7 years ago

@vmasto Potentially it's because submit events bubble.

mrchief commented 7 years ago

wrapper.find(...).get(...).click is not a function

Enzyme v3 with react 16 adapter also throws this.

Schachte commented 6 years ago

@mrchief did you find a solution for this?

mrchief commented 6 years ago

@Schachte I think I used these instead:

wrapper.find('form').simulate('submit')

// or

wrapper.find('.btn-text').simulate('click')  // btn-text is simply an example. you can use your own selector
tresor616 commented 6 years ago

Instead of simulating the submit event on the form you could simulate the event from the button itself.

wrapper.find('button').simulate('submit');

majew7 commented 6 years ago

@cppbit I don't think this is a solution, because this enzyme approach doesn't honor the type of the rendered <button> within mount().

In my situation, I have a React component that is a wrapper for an HTML <button>. And I want the <button> type to be dynamic, as chosen by the wrapper component. I was hoping to test the correct rendering and behavior, by seeing of the <form> onSubmit callback is called (or not called).

tresor616 commented 6 years ago

@majew7 from what you are describing it doesn’t sound like your scenario is the same as the one in the topic of this thread which I was contributing towards. In this scenario it’s a form with an onSubmit and the corresponding static submit button type which is predictable.

Perhaps share your code snippet and we can help you out with your specific scenario where the button has a dynamic type within your wrapper component. The event simulation is a signal sent to a component, if the component is not expecting the event then it won’t work.

ljharb commented 6 years ago

This is sadly working as intended. simulate does not faithfully simulate anything - it's just sugar for invoking a prop function with a mapped event name.

I'm going to close this with a recommendation to avoid simulate entirely.

onurarpapay commented 6 years ago

hello, well I was struggling with simulate('submit') for one full day. It was working, i.e. it was raising the submit event but I always got a "TypeError: handler.apply is not a function." Issue is solved with only changing 'submit' to 'onsubmit'. I don't understand why. 'onSubmit' also works. So it is:

form.simulate('onsubmit')

ljharb commented 6 years ago

@onurarpapay it shouldn't work; the event name is "submit", not "onsubmit". Can you file a new issue about that one?

MartinDawson commented 5 years ago

Can somebody post a full working example please.

I have tried all the suggested solutions above using mount with a simple type submit button inside a form with onSubmit and it does not work still.

MartinDawson commented 5 years ago

Also, is this possible to do by simulating a keypress of enter on the submit button?

ljharb commented 5 years ago

@MartinDawson no, it’s not - and your test should be testing that the onSubmit prop does what you expect. Your test should not be verifying that the browser, or react itself, works properly - that’s a job for those projects’ tests.

MartinDawson commented 5 years ago

@ljharb I understand that but I was trying to do behavior driven development so I thought it would have been better to try and do it as close as possible to browser.

ljharb commented 5 years ago

In that case, you'd want something like nightwatch/selenium or puppeteer/cypress, not a unit testing tool like enzyme.

mrchief commented 5 years ago

@ajc24 I like how your post started but then, you end up testing the browser, not your unit. We all know that a submit button inside a form will fire the submit event and it's the browser's job to test that part. You should rather be testing that the submit handler does what it's supposed to and if you can ensure that, then you can leave testing the browser out of your unit tests and still be ok. Am I missing something?

Peripona commented 5 years ago

Working Solution - React Enzyme jest component Test

sharing an online working good short example. Well the problem is not just form submit it could be any input type where the attached event handler is using the event to get the target value or even a preventDefault. So i am going to attach a full code example of how we can test it. ReactComponentTestWithJestAndEnzyme

In this example i have created a Checkbox and attached an event Handler, when one triggers the event onChange if we are expecting anything other than this which in this case is event. the test calling the simulate need to mock this. or just to say pass the target object with value. Just go to the Tests tab and see the test running there. found in the CheckboxWithLabel.test.js file.

Hope this helps someone. ❇️

mrchief commented 5 years ago

@Peripona Thanks for posting the sandbox, it's going to make it easy for people to try out different solutions. (I also love your handle "Peri-Pona" :))

I've since moved away from relying on simulate. I don't need to test the browser's part of firing the event, nor the React's part of calling my event handler when that happens. Instead, what I actually want to test is when my event handler is called, it does its job correctly. So I use wrapper.instance().onChange(...) to test the handlers - Updated CodeSandbox.

This is not a silver bullet and whether it's the right approach or not depends on one's use case; e.g. one might argue that reaching out to wrapper.instance().onChange is relying too much on internal mechanisms and that would be a valid argument. Another argument would be that wrapper.instance will not work for functional components (in those cases you can simply export the handler and test it out separately).

I'm not sure if there is an absolute right or wrong in this case or even one way is more right than the other - just that we have options for those who get to this issue searching for answers. I hope @ljharb can chime on whether relying on wrapper.instance is a good idea or not.

ljharb commented 5 years ago

@mrchief it's totally fine for your tests for a given component, to rely on that component's instance (and it having one). In general, I'd suggest never using simulate, and directly invoking prop functions. One solution is to directly test that invoking those props does the right thing; or you can mock out instance methods, test that the prop functions call them, and unit test the instance methods. Either is fine, and it'll depend on what your code is doing.

mliq commented 5 years ago

My solve is this: As long as <button> has null or undefined for onClick method, a click will propagate to the parent.

So, I am testing for that directly:

describe('<Button> with no handleClick prop', () => {
    const wrapper = shallow(<Button>{MOCK_CONTENT}</Button>);

    it('renders with null or undefined onClick method (allowing click to bubble up to form onSubmit)', () => {
        expect(wrapper.prop('onClick')).toBeFalsy();
    });
});
vjekofalco commented 5 years ago

The form submits if you use the submit event on the button.

const onSubmit = sinon.spy();
const wrapper = mount(
    <form onSubmit={onSubmit} />
);
const button = wrapper.find('button');
button.simulate('submit');

The form submits if you use the submit event on the button.

const onSubmit = sinon.spy();
const wrapper = mount(
    <form onSubmit={onSubmit} />
);
const button = wrapper.find('button');
button.simulate('submit');

If you have disabled property on button you will submit the form no mater if it is disabled or not.

cristian-eriomenco commented 3 years ago

This is sad.

JSEvgeny commented 3 years ago

Instead of simulating the submit event on the form you could simulate the event from the button itself.

wrapper.find('button').simulate('submit');

Thank you very much!!!

MeroVinggen commented 2 years ago

Found solution for me:

Switched onFinish form callback to onClick callback on submit button:

onClick={() => form.validateFields().then(<onFinishCallback>, ()=>{})}

In test:

it('', async() => {
  ...
  // without await not working, you can switch it to await new Promise((res) => setTimeout(res, 50)); after act also wotk
  await act(async () => {
    <submitButton>.simulate("click");
  });
});