Shivam Sahil
Shivam Sahil

Reputation: 4921

Dispatch a Custom Event and test if it was correctly triggered (React TypeScript, Jest)

I am trying to validate a custom event listener that lies inside a react function useEffect hook as shown below:


export interface specialEvent extends Event {
    detail?: string
}

function Example() {
    React.useEffect(()=>{
         document.addEventListener('specialEvent', handleChange)
         return () => {
               document.removeEventListener('specialEvent',handleChange)
          }
    })
    const handleChange = (event:SpecialEvent) => {
       ...
      }

}

I want to trigger this custom event listener and test it in jest:

it('should trigger "specialEvent" event Listener Properly', async () => {
        const specialEvent = new CustomEvent('specialEvent')
        const handleChange = jest.fn()
        render(<Example />)
        await waitFor(() => {
            window.document.dispatchEvent(specialEvent)
            expect(window.document.dispatchEvent).toHaveBeenNthCalledWith(1, 'specialEvent')
            expect(specialEvent).toHaveBeenCalledTimes(1)
        })
    })

This code gives me the following error:

expect(received).toHaveBeenNthCalledWith(n, ...expected)

    Matcher error: received value must be a mock or spy function

    Received has type:  function
    Received has value: [Function dispatchEvent]

As suggested in one of the answers, I tried this:

//Assert Statements

const specialEvent = new CustomEvent('specialEvent');
const handleSelect = jest.fn();
act(() => { 
  render(<Example />) 
});
await waitFor(() => { 
  window.document.dispatchEvent(specialEvent) 
  expect(handleSelect).toHaveBeenCalledTimes(1) 
});

But this time it says expected call to be 1 but recieved 0.

Can someone help me resolving this?

Upvotes: 1

Views: 9287

Answers (2)

Lin Du
Lin Du

Reputation: 102307

When testing, code that causes React state updates should be wrapped into act(...). If the handleChange does not cause the React state to update, you don't need to use act.

Besides, it's better not to test the implementation detail, for your case, the test implementation detail statements are:

expect(window.document.dispatchEvent).toHaveBeenNthCalledWith(1, 'specialEvent')
expect(specialEvent).toHaveBeenCalledTimes(1)

Every small change to the implementation detail will cause the test case need to be modified. We should test the UI from the perspective of the user, who doesn't care about the implementation details of the UI, only about rendering the UI correctly.

What you should test is: what happens to the output of the component when the custom event is fired and the state is changed in the event handler.

E.g.

index.tsx:

import React, { useState } from 'react';

export interface SpecialEvent extends Event {
  detail?: string;
}

export function Example() {
  const [changed, setChanged] = useState(false);
  React.useEffect(() => {
    document.addEventListener('specialEvent', handleChange);
    return () => {
      document.removeEventListener('specialEvent', handleChange);
    };
  });
  const handleChange = (event: SpecialEvent) => {
    console.log(event);
    setChanged((pre) => !pre);
  };
  return <div>{changed ? 'a' : 'b'}</div>;
}

index.test.tsx:

import { render, screen, act } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import React from 'react';
import { Example } from './';

describe('70400540', () => {
  test('should pass', () => {
    const specialEvent = new CustomEvent('specialEvent');
    render(<Example />);
    expect(screen.getByText('b')).toBeInTheDocument();
    act(() => {
      window.document.dispatchEvent(specialEvent);
    });
    expect(screen.getByText('a')).toBeInTheDocument();
  });
});

window.document.dispatchEvent(specialEvent) will cause the React state to change, so we wrap it into act(...).

Test result:

 PASS  examples/70400540/index.test.tsx (11.259 s)
  70400540
    ✓ should pass (59 ms)

  console.log
    CustomEvent { isTrusted: [Getter] }

      at Document.handleChange (examples/70400540/index.tsx:16:13)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        12.655 s

package versions:

"@testing-library/react": "^11.2.2",
"react": "^16.14.0",
"jest": "^26.6.3",

Upvotes: 4

Phil Plummer
Phil Plummer

Reputation: 1

As the error message says, the toHaveBeenNthCalledWith matcher requires a mock or spy to be passed to expect.

However, you probably do not need to make any assertion about window.document.dispatchEvent being called because you know you are calling it on the line above in your test.

For more information, check the docs on toHaveBeenNthCalledWith here: https://jestjs.io/docs/expect#tohavebeennthcalledwithnthcall-arg1-arg2-

Upvotes: 0

Related Questions