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

Unable to simulate keyboard events #441

Open
darkjoker opened this Issue Jun 7, 2016 · 30 comments

Comments

@darkjoker

darkjoker commented Jun 7, 2016

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

This comment has been minimized.

Collaborator

aweary commented Jun 7, 2016

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

@darkjoker

This comment has been minimized.

darkjoker commented Jun 7, 2016

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

This comment has been minimized.

Collaborator

aweary commented Jun 7, 2016

@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.

@darkjoker

This comment has been minimized.

darkjoker commented Jun 8, 2016

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

This comment has been minimized.

hamishtaplin commented Sep 6, 2016

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

@darkjoker

This comment has been minimized.

darkjoker commented Sep 6, 2016

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

@aweary

This comment has been minimized.

Collaborator

aweary commented Sep 6, 2016

@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.

@darkjoker

This comment has been minimized.

darkjoker commented Sep 6, 2016

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

@springuper

This comment has been minimized.

springuper commented Sep 13, 2016

+1, waiting for a solution.

@pranjalk

This comment has been minimized.

pranjalk commented Oct 17, 2016

@darkjoker @springuper try this, worked for me

wrapper.find('.myclass').simulate('keyDown', { key: 'ArrowLeft' });
@jblok

This comment has been minimized.

jblok commented 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.

@abhishek1nair

This comment has been minimized.

abhishek1nair commented Dec 15, 2016

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

This comment has been minimized.

simeg commented Feb 9, 2017

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

This comment has been minimized.

tomitrescak commented Mar 6, 2017

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

@ljharb

This comment has been minimized.

Member

ljharb commented Mar 6, 2017

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

@prijuly2000

This comment has been minimized.

prijuly2000 commented Mar 30, 2017

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

@danyim

This comment has been minimized.

danyim commented Jul 14, 2017

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

@pranjalk

This comment has been minimized.

pranjalk commented Jul 14, 2017

@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

This comment has been minimized.

EduardoAC commented Aug 4, 2017

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

This comment has been minimized.

pranjalk commented Aug 4, 2017

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

@EduardoAC

This comment has been minimized.

EduardoAC commented Aug 4, 2017

@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.

@ryanhomer

This comment has been minimized.

ryanhomer commented Sep 1, 2017

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 added a commit to eugene-matvejev/battleship-game-gui-react-js that referenced this issue Sep 15, 2017

@eugene-matvejev

This comment has been minimized.

eugene-matvejev commented Sep 15, 2017

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

This comment has been minimized.

hzhu commented Feb 15, 2018

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

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

@elhay-av

This comment has been minimized.

elhay-av commented Jun 12, 2018

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

This comment has been minimized.

tomitrescak commented Jun 12, 2018

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

This comment has been minimized.

marcysutton commented Jun 19, 2018

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

This comment has been minimized.

AutoSponge commented Jun 22, 2018

@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

This comment has been minimized.

Member

ljharb commented Jul 4, 2018

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

@jherax

This comment has been minimized.

jherax commented Nov 26, 2018

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment