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

window.addEventListener not triggered by simulated events #426

Closed
timoxley opened this issue May 28, 2016 · 21 comments
Closed

window.addEventListener not triggered by simulated events #426

timoxley opened this issue May 28, 2016 · 21 comments

Comments

@timoxley
Copy link

The first component I tried to test using this was a mixin for detecting clicks outside a component. In order to do this one needs to listen with window.addEventListener('click'). This handler doesn't appear to be triggered when using enzyme simulated clicks.

If handling this is out of scope for enzyme, do you have a recommendation on the best way to get this under test?

@timoxley
Copy link
Author

timoxley commented May 28, 2016

Ahh, so mount doesn't actually attach the rendered fragment to the document, so even if simulated events did bubble up through the concrete DOM, they wouldn't reach window anyway.

I found a workaround:

  • Attach the mounted component to document.body
  • Use simulant to fire "real" DOM events.

e.g.

  it('only triggers clickOutside handler when clicking outside component', t => {
    const onClickOutside = sinon.spy()
    mount(<Page onClickOutside={onClickOutside} />, { attachTo: document.body })
    simulant.fire(document.body.querySelector('aside'), 'click')
    t.equal(onClickOutside.callCount, 1, 'should fire when clicking menu sibling')
    document.body.innerHTML = ''
    t.end()
  })

However, I've just noticed that in this particular example enzyme isn't actually doing anything haha.

We can just ReactDOM.render into our JSDOM document.body directly:

  it('only triggers clickOutside handler when clicking outside component', t => {
    const onClickOutside = sinon.spy()
    ReactDOM.render(<Page onClickOutside={onClickOutside} />, document.body)
    simulant.fire(document.body.querySelector('aside'), 'click')
    t.equal(onClickOutside.callCount, 1, 'should fire when clicking menu sibling')
    document.body.innerHTML = ''
    t.end()
  })

Curious now as to why not use just always use this method? why enzyme?

@tleunen
Copy link

tleunen commented Jun 14, 2016

Can a core contributor respond to this?
I'm having the same issue now where instead of attaching a event on a direct element, I'm attaching it on the document so that I can easily detect a click outside of the said element. But I'm having hard time testing this behavior with enzyme.

@aweary @lelandrichardson

@aweary
Copy link
Collaborator

aweary commented Jun 14, 2016

@tleunen I doubt this is a use case we'd support, enzyme is meant to test React components and attaching an event listener to the document with addEventListener means the event is not being handled by React's synthetic event system. Our simulate method for mount is a thin wrapper around ReactTestUtils.Simulate, which only deals with React's synthetic event system.

I can't speak to your specific use case, but I would advise that this is generally an anti-pattern in React and should be handled within React's event system when possible (such as passing down an onClick prop from a stateful parent and calling it in the leaf component's onClick handler). You can try workaround like @timoxley but your mileage may vary.

@aweary aweary closed this as completed Jun 14, 2016
@tleunen
Copy link

tleunen commented Jun 14, 2016

So you would attach an onClick on the main root component, and then passing it to all components? Maybe using the context then?

@aweary
Copy link
Collaborator

aweary commented Jun 14, 2016

That may be a good use case for context if you need to monitor click events within arbitrarily nested components.

@rpmonteiro
Copy link

rpmonteiro commented Jun 25, 2016

@aweary , would you mind giving a bit more information about how to tackle those window/body event listeners using context? I've been searching endlessly as to how's the best way of doing it and I don't have an answer yet.

I'm having exactly the same problem as @timoxley , but with a KeyPress event... It's proving to be extremely frustrating to test.

Thank you

@aweary
Copy link
Collaborator

aweary commented Jun 25, 2016

@rpmonteiro if you were using context you wouldn't be using the native event system, you would have on onKeyPress method on your top-level component that you make available via context to child components. I'm not sure if this is the best idea, and I haven't tried it. With that disclaimer, it might look something like:

class App extends React.Component {
  getChildContext() {
    onKeyPress: this.onKeyPress
  }

  onKeyPress(event) {
  // handle event here
  }
}

Then in some child component

class SomeChildComponentOfApp extends React.Component {
   static contextTypes = {
     // the onKeyPress function is now available via `this.context.onKeyPress`
     onKeyPress: React.PropTypes.func
   }
}

If you absolutely must use the native event system, you might look into another library for mocking addEventListener and the related functions.

@blainekasten
Copy link
Contributor

Generally any test framework(jest, mocha, etc) can solve your problem natively. Your goal here is effectively to make sure the event is bound, and that when its fired something happens in your component. So you'll have to do some setup prior to rendering, but it is definitely possible to test this code without using context.

To be clear, @aweary is spot on in saying this is not enzyme supported.

For example in jest this is a sort of code you could use.

const map = {};
Window.addEventListener = jest.genMockFn().mockImpl((event, cb) => {
  map[event] = cb;
});

// render component

map.event(...args);

// assert changes

@victorhqc
Copy link

victorhqc commented Oct 13, 2016

@blainekasten you saved my day :)

const map = {};
window.addEventListener = jest.genMockFn().mockImpl((event, cb) => {
  map[event] = cb;
});

const component = mount(<SomeComponent />);
map.mousemove({ pageX: 100, pageY: 100});

This worked for me, the state of the component is successfully being updated.

@kellyrmilligan
Copy link

just as a small update and FYI, for document this is working for me, and on a newer version of jest:

const map = {};
    document.addEventListener = jest.fn((event, cb) => {
      map[event] = cb;
    })

@LVCarnevalli
Copy link

Component:


componentDidMount() {   
 ReactDOM.findDOMNode(this.datePicker.refs.input).addEventListener("change", (event) => {
    const value = event.target.value;
    this.handleChange(Moment(value).toISOString(), value);
  });
}

Test:


it('change empty value date picker', () => {
    const app = ReactTestUtils.renderIntoDocument(<Datepicker />);
    const datePicker = ReactDOM.findDOMNode(app.datePicker.refs.input);
    const value = "";

    const event = new Event("change");
    datePicker.value = value;
    datePicker.dispatchEvent(event);

    expect(app.state.formattedValue).toEqual(value);
});

@prasadmsvs
Copy link

prasadmsvs commented Feb 1, 2018

I'm sorry I am a newbie to react and jest. I didn't understand what is happening here

const map = {};
window.addEventListener = jest.genMockFn().mockImpl((event, cb) => {
  map[event] = cb; // what is cb here?
});

const component = mount(<SomeComponent />);
map.mousemove({ pageX: 100, pageY: 100}); // what is map here?

How did it effect component behaviour?

@ljharb
Copy link
Member

ljharb commented Feb 2, 2018

@prasadmsvs please file a new issue rather than commenting on a new one; but for this kind of question, the gitter channel linked in the readme is preferred.

@vedraan
Copy link

vedraan commented Aug 17, 2018

Inspired by @kellyrmilligan's solution here's full implementation I use to detect ESC keydown (also useful for any other event type):

it('calls the dismiss callback on ESC key', () => {
  const KEYBOARD_ESCAPE_CODE = 27;
  const mockDismissCallback = jest.fn();

  // Declaring keydown prop so the linter doesn't compain
  const eventMap = {
    keydown: null,
  };

  document.addEventListener = jest.fn((event, cb) => {
    eventMap[event] = cb;
  });

  // MyModalComponent internally uses
  // document.addEventListener('keydown', this.onModalDialogKeyDown, false);
  // which then via onModalDialogKeyDown binding does some stuff and then calls onDismiss which
  // is really mockDismissCallback
  const modal = shallow(
    <MyModalComponent isOpen={true} onDismiss={mockDismissCallback}>
      Test
    </MyModalComponent>
  );

  eventMap.keydown({ keyCode: KEYBOARD_ESCAPE_CODE });

  expect(mockDismissCallback.mock.calls.length).toEqual(1);
});

@awreese
Copy link

awreese commented Sep 5, 2018

Testing a React app I had to simulate a string of text input, arrived at this solution using sinon:

const sandbox = sinon.createSandbox();

let handlers = [];
const fakeAddListener = (type, handler) => handlers.push(handler);
const dispatchKeypress = (e) => handlers.forEach(h => {h(e)});

// My handlers only examine the `which` property, but construct event with the properties you need
const createKeypressEvent = (c) => ({ which: c.charCodeAt(0) });

const simulateKeyStrokes = (textString) => [...textString].forEach(c => dispatchKeypressEvent(createKeypressEvent(c)));

beforeEach(() => {
    handlers = [];
    sandbox.stub(document, 'addEventListener').callsFake(fakeAddListener);
});

afterEach(() => {
   sandbox.restore();
});

it('simulates a bunch of key strokes', () => {
    // instantiate react component test object here that attaches listeners
    // setup any stubs/spies

    simulateKeyStrokes('abcdefghijklmnopqrstuvwxyz');

    // handle assertions/expects
});

@Faline10
Copy link

Faline10 commented Oct 18, 2018

This also works without having to mock window.addEventListener, due to all the reasons mentioned above. Test framework: jest + enzyme.

    test('should close when user clicks outside it', () => {
        const outerNode = document.createElement('div');
        document.body.appendChild(outerNode);
        
        const onClose = jest.fn();
        const wrapper = mount(<Toast onClose={ onClose } />, { attachTo: outerNode });
        const toast = wrapper.find(`[data-test-id="toast"]`); // getDOMNode() could also work

        toast.instance().dispatchEvent(new Event('click'));
        expect(onClose).not.toHaveBeenCalled();

        outerNode.dispatchEvent(new Event('click'));
        expect(onClose).toHaveBeenCalled();
    });

A little further explanation: wrapper.find().instance() returns a DOM element (whereas wrapper.instance() would just return the Toast class)--this gives us access to EventTarget.dispatchEvent(), which you can use to dispatch non-synthetic events, rather than mocking out window.addEventListener.
And by adding another div to the document.body, then attaching the mounted wrapper, you ensure the real element event will bubble up to the actual window. (Assumption is that you have a click listener on the window.) Note that I also tried attaching directly to the document.body, as in the first comment, but React then throws "Rendering components directly into document.body is discouraged."

@markmcdermid
Copy link

@Faline10, that's awesome! As a note to others, I had to do .dispatchEvent(new Event('click', { bubbles: true})) to make it bubble to the window

@riyaz4s
Copy link

riyaz4s commented Jan 25, 2019

@Faline10, that's awesome! As a note to others, I had to do .dispatchEvent(new Event('click', { bubbles: true})) to make it bubble to the window

ev = new Event('click');

I am getting Event is undefined.

@feargalObo
Copy link

feargalObo commented Dec 12, 2019

@Faline10 Trying to implement your method. But sporadically running into this error when the test is run.
TypeError: p.instance(...).dispatchEvent is not a function
And here is the test.

  it('clicking outside of cell deselects cell', () => {
    const callBack = jest.fn();
    const thisWrapper = mount(
      <ObjectItemEditableSelectCell
        {...mockConnectProps}
        handleClick={callBack}
      />,
    );
    const p = thisWrapper.find(Layout);
    console.log('p.debug()', p.debug());
    p.instance().dispatchEvent(new Event('click'), { bubbles: true } );
    thisWrapper.update();
    expect(thisWrapper.state('selectedCell')).toBe(true);
  });

Would be grateful to anyone who can help.

@ljharb
Copy link
Member

ljharb commented Dec 12, 2019

@feargalObo In this case, p.instance() won't be an HTML element, it will be a Layout component instance. You may want to locate a specific DOM element first.

@ljharb
Copy link
Member

ljharb commented Dec 12, 2019

@feargalObo let's move these comments to a new issue.

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