enzymejs / enzyme

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

Unable to simulate keyboard events #441

Open ghost opened 8 years ago

ghost commented 8 years ago

Problem

I'm truing to simulate keyboard events with enzyme but I couldn't find a single line of documentation or code example where keyboard events are implemented. I've tried using

wrapper.find(".myclass").simulate("keyDown", {
            target: {
                keyCode: 40,
                which: 40,
                key: "down arrow"
            }
        });

and

wrapper.find(".myclass").simulate("keyDown", {
                keyCode: 40,
                which: 40,
                key: "down arrow"

        });

I've also tried using other types of casing on the event name and key names but nothing worked.

While there's no error using any of the two examples I've mentioned the output just isn't the expected and using TestUtils.Simulate.keyDown(searchInput, { keyCode: 40 }); all works as expected. Am I not using the correct syntax?

aweary commented 8 years ago

@darkjoker are you using shallow or mount? Can you share the code for the React component you're trying to test?

ghost commented 8 years ago

I've tried both after reading several posts, but i couldn't find a way to make the keydown, keypress or keyup events to work.

aweary commented 8 years ago

@darkjoker you can see a working example that I wrote this morning to verify the behavior: https://github.com/Aweary/enzyme-test-repo/blob/issue-441/test.js


class TestComponent extends React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    return (
      <div>
        <input onKeyDown={this.props.onKeyDown} />
      </div>
    );
  }
}

describe('Issue #441', () => {
  it('It should simulate keydown events', () => {
    const onKeyDown = sinon.spy();
    const wrapper = mount(<TestComponent onKeyDown={onKeyDown}/>);
    const input = wrapper.find('input');
    input.simulate('keyDown', {keyCode: 40});
    expect(onKeyDown.called).to.be.true;
  });

This passes as I'd expect. If you can share a simplified reproducible case where it's not working that would be great. Feel free to fork that ^ repo to do so, if you'd like.

ghost commented 8 years ago

Thnaks for the quick response, the component code is in the attachement, the test code is as it follows:

describe("ComboBox Component", () => {

    let options = [
        { "value": 0, "text": "United States" },
        { "value": 1, "text": "United Kingdom" },
        { "value": 16, "text": "Slovakia" }
    ];

    it("it highlights option selected with keyboard on option on list", () => {
        let combo = mount(
            <ComboBox comboBoxElements={options}/>
        );

        // TestUtils.Simulate.keyDown(searchInput, { keyCode: 40 });
        combo.find(".fa-chevron-down").simulate("click");

        let input = combo.find("input");
        input.simulate("keyDown", { keyCode: 40 });
        expect(combo.find("li").at(1).prop("className")).toBe(("selectedOption selectedChildren"));

    });

});

ComboBox.zip

hamishtaplin commented 7 years ago

@darkjoker Did you resolve this? I'm seeing a similar problem.

ghost commented 7 years ago

No, I have been using TestUtils for this kind of tests

aweary commented 7 years ago

@darkjoker for mount, the simulate method is a thin wrapper around ReactTestUtils.Simulate, so I feel like there's something else going on with your component that's causing it to fail. Can you share a simplified case reproducing the issue? The ComboBox is rather large (and compiled) so it's hard to parse what's going on there.

ghost commented 7 years ago

I'll post a simplified version of the component asap only maintaining the core functionality I'm trying to test

springuper commented 7 years ago

+1, waiting for a solution.

pranjalk commented 7 years ago

@darkjoker @springuper try this, worked for me

wrapper.find('.myclass').simulate('keyDown', { key: 'ArrowLeft' });
jblok commented 7 years ago

For me the problem was I had added event listeners on the DOM elements themselves, and not in JSX (e.g. <div onKeyDown={} />. I guess the simulate method only works when events are registered on the components themselves, not the underlying DOM nodes.

abhishek1nair commented 7 years ago

You can refer to mapNativeEventNames function in enzyme to know the correct key to use for each action: https://github.com/airbnb/enzyme/blob/master/src/Utils.js#L318. This just mocks the event object, so if you're using event.keyCode in your code, just pass { keyCode: 40 } as second parameter to simulate.

simeg commented 7 years ago

It did not work for me until I used the which property.

wrapper.find('.myclass').simulate('keyDown', { key: 'Tab', keyCode: 9, which: 9 });

I can remove both key and keyCode properties and it still works, but it's nice to see in the test what key it refers to.

tomitrescak commented 7 years ago

Is it possible to simulate multiple keys, such as ctrls+s? Sorry for breaking into this discussion though :/

ljharb commented 7 years ago

@tomitrescak that would require a keypress for s with ctrlKey set to true, iirc.

prijuly2000 commented 7 years ago

I am having the same problem with the keyPress event. All other events can be simulated except key events on text input field

danyim commented 7 years ago

Having the same issue here. None of the solutions above worked for me.

pranjalk commented 7 years ago

@danyim @prijuly2000 if you use onChange to simulate input fields, only the last character in the string will be reflected in the keyDown event so you can use that to test keyPress

So you can slice the string input and check for each last character to simulate a simulation on keyPress via keyDown events!

import React, { Component } from 'react';
import { mount } from 'enzyme';
import { expect } from 'chai';

export class Issue441 extends Component {
  constructor() {
    super();
    this.state = {
      change: '',
      keyDown: ''
    }
    this.onChangeInput = this.onChangeInput.bind(this);
    this.onKeyDownInput = this.onKeyDownInput.bind(this);
  }
  onChangeInput(e) {
    this.setState({
      change: e.target.value
    })
  }
  onKeyDownInput(e) {
    this.state({
      keyDown: e.keyCode
    });
    // to check which of the following actually works on your system
    if(e.keyCode)
      console.log(e.keyCode)
    if(e.key)
      console.log(e.key)
    if(e.which)
      console.log(e.which)
  }
  render() {
    return (
      <div>
        <input
          class='myclass'
          onChange={this.onChangeInput}
          onKeyDown={this.onKeyDownInput}
        />
      </div>
    );
  }
}

describe('<Issue441/>', () => {
  const wrapper = mount(<Issue441/>);
  it('should simulate change', () => {
    wrapper.find('input.myclass').simulate('change', { target: { value: 'xyz' } });
    expect(wrapper.state('change')).to.equal.('xyz');
    expect(wrapper.state('keyDown')).to.equal.('z');
  });

  // theoretical testing of keyPress events
  const st = 'string';
  for(let i in st) {
    it('should theoretical simulate keyPress', () => {
      wrapper.find('input.myclass').simulate('change', { target: { value: st.slice(0, i += 1) } });
      expect(wrapper.state('change')).to.equal.(st.slice(0, i += 1));
      expect(wrapper.state('keyDown')).to.equal.(st[i]);
    });
  }
})

NOTE

for(let i in st) is the same as for(i=0; i < st.length; i++) 0, 1, 2, 3, 4, 5 for(let i of st) will return i as values of string s, t, r, i, n, g

EduardoAC commented 7 years ago

It's counter intuitive that for testing a keyDown event that you need to use change. Personally, I believe that you should have the option to simulate all the events even if it's only an interface that will execute change behind scenes.

pranjalk commented 7 years ago

@EduardoAC the example is to mock testing of keyPress events and not keyDown events

EduardoAC commented 7 years ago

@aweary I managed to get it working on mount by using

.simulate('keyDown', { key: 'Enter', keyCode: 13, which: 13 })

However, the same example using shallow rendering didn't work, Is any reason why shallow render doesn't allow to handle keyDown?

@EduardoAC the example is to mock testing of keyPress events and not keyDown events

@pranjalk Honestly I don't know where to start to comment your post even if you example achieve the question asked. Personally, it's over engineer from my point of view because you are storing the event on the state just for test that the event gets fire with the value expected.

Otherwise, you should target examples like "Brandon dail" propose, but replacing keyDown for keyPress.

techrah commented 6 years ago

jblok commented on Dec 8, 2016 For me the problem was I had added event listeners on the DOM elements themselves, and not in JSX (e.g. <div onKeyDown={} />. I guess the simulate method only works when events are registered on the components themselves, not the underlying DOM nodes.

I believe this is true. The simulate function will only trigger the exact event handler that you specify. It is not doing a real simulation that causes events to get triggered but rather actually calling the specified event handler directly instead. For real simulation, you may need to use something like simulant.

eugene-matvejev commented 6 years ago

Hello guys,

thanks for the enzyme!

here is some problem what I try to describe.

I got battleship game, as side project where I report bugs and try things. so this PR: https://github.com/eugene-matvejev/battleship-game-gui-react-js/pull/126/files when your component is mounted it add event listener on '<' and '>' arrows to switch pages backwards and forwards

if I remove it here: [as it doesn't work to be honest, as we need observer 'document level'] https://github.com/eugene-matvejev/battleship-game-gui-react-js/pull/125/files [but document 'level' binding is in Mount/Unmount]

test fails

do I'm doing something wrong? or how I could emulate 'document level' events? I presume because react's events are syntetic, it is impossible?

hzhu commented 6 years ago

I was attempting to simulate a ArrowDown keydown event on an <input />.

@pranjalk your solution worked for me with Enzyme 3. Thanks!

elhay-av commented 6 years ago

i found that if the events attached by element.addEventListener() .simulate() will not trigger the listener.

so my suggested solution is to simulate the event manually by element.dispatchEvent()

const simulateKeypress = (element, key) => {
  let code = key.charCodeAt(0);
  const event = new KeyboardEvent('keypress', {key: key, code, charCode: code, keyCode: code});
  element.dispatchEvent(event);
};
tomitrescak commented 6 years ago

Considering that you can trust the HTML event to raise the OnClick or OnChange even when requested, this is all unnecessary. Just call the OnChange prop of the React element.

marcysutton commented 6 years ago

I believe this is true. The simulate function will only trigger the exact event handler that you specify. It is not doing a real simulation that causes events to get triggered but rather actually calling the specified event handler directly instead. For real simulation, you may need to use something like simulant.

I sure wish you could trigger a click event from the keyboard for accessibility testing using buttonComponent.simulate('keydown', {which: 13}) or similar, like real DOM nodes do. It's a great way to assert an HTML element is focusable and works from the keyboard, rather than testing only for mouse clicks. onClick bindings never respond from key events in Enzyme with JSDOM, and binding to both keydown and mousedown just for testing purposes creates unnecessary complexity.

This is one of the hairiest accessibility problems I've come across in React testing. I've tried every relevant trick I could come up with, and I'm having to concede and write keyboard compatibility tests in Selenium Webdriver instead. Even Simulant doesn't seem to trigger event callbacks in this scenario, I suspect because of the way SyntheticEvent is delegating events through the DOM tree rather than on a specific button node.

AutoSponge commented 6 years ago

@marcysutton I think our team is going to have to start using syntax like this to make it clear:

function handler(event) {
  const evt = event.nativeEvent || event // works for React events
  const key = evt.code || evt.which // prefers non deprecated api
  ...
}

It may be worth it to remove .which all together to force older test scripts to get updated.

ljharb commented 6 years ago

simulate should be avoided. It does not faithfully simulate anything - it's just sugar for .prop('onClick')() or similar.

jherax commented 5 years ago

When working with events created by addEventListener(), it seems that simulate() does not work properly ( using mount()), e.g.

class AwInput extends Component {
  inputElement = null;

  componentDidMount() {
    const node = this.inputElement;
    // using some third-party library to manipulate the DOM node.
    node.addEventListener("blur", innerEventHandler);
    node.addEventListener("keydown", innerEventHandler);
  }

  render() {
    const props = this.props;
    return (
      <inputWrapper
        id={props.id}
        value={props.value}
        onChange={props.onChange}
        innerRef={(el) => (this.inputElement = el)}
      />
    );
  }
}

function innerEventHandler (event) {
  // simulate() never reaches this point
  console.log('innerEventHandler:', event);
}

And writing the unit test for that component with Enzyme...

describe('Testing <AwInput />', () => {
  const ctx = { value: 'testing' };
  const onChange = jest.fn((e) => (ctx.value = e.target.value));

  const Wrapper = mount(
    <AwInput
      id="awInput"
      value={ctx.value}
      onChange={onChange}
    />
  );

  const inputElement = Wrapper.find('#awInput').last();

  it('Should render input element without any error', () => {
    expect(inputElement.exists()).toBe(true); // success
    expect(inputElement).toHaveLength(1); // success
  });

  it('Should call the mock onChange function', () => {
    const value = 'new value';
    inputElement.simulate('change', { target: { value } }); // success
    expect(onChange).toHaveBeenCalled();
    expect(ctx.value).toBe(value);
  });

  it('Should call the inner keyDown function', () => {
    inputElement.simulate('keyDown', { key: 'a', keyCode: 97 }); // fail
    // expected: console.log('innerEventHandler:', event);
  });
});

So, as mentioned before, it seems the simulate() method only works with events registered on the components themselves, but when dealing with events attached to the underlying DOM nodes, it does not work.

Using ReactTestUtils.Simulate is not working as expected neither :(

BTW, it's worth mentioning the Enzyme Future Work.

codingarrow commented 5 years ago

class TestComponent extends React.Component {

  constructor(props) {
    super(props);
  }

handleKDown = e => { if (e.key === 'Enter') { this.handleSubmit(); } };

render() { return (

);

} }

describe('Issue #441', () => { it('It should simulate keydown events', () => { const onKeyDown = sinon.spy(); const wrapper = mount(); const input = wrapper.find('input'); input.simulate('keyDown', {keyCode: 40}); expect(onKeyDown.called).to.be.true; });



This passes as I'd expect. If you can share a simplified reproducible case where it's not working that would be great. Feel free to fork that ^ repo to do so, if you'd like.

Thank you what if I added an IF and handleKDown function (which calls submit) then the code will not be covered using jest, how do I make assertions for that please?

So far I got

const handleKDown = jest.fn();
 ......
      .simulate('keydown', { 'keyDown', {keyCode: 13});
expect(handleKeyDown.mock.calls[0]).toBeCalled();

But it does not work, it kept saying 'undefined' or expected mockCall function ??? which is alienating to me

jherax commented 5 years ago

@codingarrow, I can see in your event-handler implementation you are using an argument, which is not the same provided in your test, let's see:

handleKDown = e => {
  if (e.key === 'Enter') {
    this.handleSubmit();
  }
};

The e parameter tries to access the .key property which is not defined in your test. So, when simulating events, pass the arguments with the expected properties, or mocked properties, let's see:

// ...
const event = {
  key: '@'
};

inputNode.simulate('keyDown', event);
expect(onKeyDown).toHaveBeenCalled();

The following example shows how to simulate the event.preventDefault() method from a simulated event:

const onKeyDown = jest.fn((e) => {
  if (e.key === '@') e.preventDefault();
});

const Wrapper = mount(
  <AwInput
    id="awInput"
    value={ctx.value}
    onKeyDown={onKeyDown}
  />
);

const inputNode = Wrapper.find('#awInput').last();

describe('Testing <AwInput />', () => {
  it('Should call onKeyDown with key "@" and isDefaultPrevented should be true', () => {
    let isDefaultPrevented = false;
    const event = {
      key: '@',
      keyCode: '@'.charCodeAt(0), // 64
      preventDefault: () => (isDefaultPrevented = true)
    };
    inputNode.simulate('keyDown', event);
    expect(onKeyDown).toHaveBeenCalled();
    expect(isDefaultPrevented).toBe(true);
  });
});

Happy testing!!

leon0707 commented 4 years ago

Is it posible to simulate shift + enter?

ljharb commented 4 years ago

@leon0707 the simulate API doesn't actually simulate anything - what you can do, however, is manually invoke an onKeyDown prop or similar, and pass a fake event object that has the right properties to mimic a shift+enter.