How can I test a component style modified by a SetTimeout fired on useEffect?

Viewed 2068

I'm using Jest/Enzyme to test a React/TypeScript application and I'm having a hard time trying to write a test to assert if a button shows up after a certain period of time:

Here's a very simplified version of the component to be tested:

import { StyledNotifyButton } from './styles'; //style-component element

const SomeComponent = (): ReactElement => {
  const [showNotifyButton, toggleNotifyButton] = useState(false);

  useEffect(() => {
    setTimeout(() => toggleNotifyButton(true), 5000);
  }, [toggleNotifyButton]);

  return (
    <div>
      <StyledNotifyButton visible={showNotifyButton} />
    </div>
  );

Here's the test:

 describe('< FailingTest >', () => {
  let wrapper: ReactWrapper;

  beforeAll(() => {
    wrapper = mount(<SomeComponent />);
  });

  it('should display the notify button only after X seconds', done => {
    let notifyButton = wrapper.find('StyledNotifyButton');

    jest.spyOn(React, 'useEffect').mockImplementation(f => f());
    expect(notifyButton.prop('visible')).toBe(false);

    jest.useFakeTimers();
    setTimeout(() => {
      wrapper.update();
      notifyButton = wrapper.find('NotifyButton');
      expect(notifyButton.prop('visible')).toBe(true);
      wrapper.unmount();
      done();
    }, 5000);
    jest.runAllTimers();
  });

I have already tries using fakeTimers, advanceTimersByTime, runAllTimers as explained in Just Timer Mocks

I've tried setTimeouts, as discussed here

Manually firing useEffect, as here or here

And many other ways... but I always get

expect(received).toBe(expected) // Object.is equality

Expected: true
Received: false

Any ideas on how to correctly get the visibility change after the timeout?? Thanks!

1 Answers

When the component is mounted, useEffect and setTimeout will be executed, you need to use jest.useFakeTimers(implementation?: 'modern' | 'legacy') before mounting the component.

Use jest.runOnlyPendingTimers() to

Executes only the macro-tasks that are currently pending (i.e., only the tasks that have been queued by setTimeout() or setInterval() up to this point)

Or jest.advanceTimersByTime(msToRun).

Besides, we need to wrap the code rendering it and performing updates inside an act() call, So put jest.runOnlyPendingTimers() in act().

Finally, we need to call wrapper.update() to make sure the state reflect to the view.

E.g.

SomeComponent.tsx:

import React, { ReactElement, useEffect, useState } from 'react';
import { StyledNotifyButton } from './styles';

export const SomeComponent = (): ReactElement => {
  const [showNotifyButton, toggleNotifyButton] = useState(false);

  useEffect(() => {
    setTimeout(() => {
      toggleNotifyButton(true);
    }, 5000);
  }, [toggleNotifyButton]);

  console.log('showNotifyButton: ', showNotifyButton);

  return (
    <div>
      <StyledNotifyButton visible={showNotifyButton} />
    </div>
  );
};

styles.tsx:

import React from 'react';

export function StyledNotifyButton({ visible }) {
  return <button>click me</button>;
}

SomeComponent.test.tsx:

import { mount, ReactWrapper } from 'enzyme';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { SomeComponent } from './SomeComponent';

describe('67440874', () => {
  let wrapper: ReactWrapper;

  beforeAll(() => {
    jest.useFakeTimers();
    wrapper = mount(<SomeComponent />);
  });
  it('should pass', () => {
    let notifyButton = wrapper.find('StyledNotifyButton');
    expect(notifyButton.prop('visible')).toBe(false);
    act(() => {
      jest.runOnlyPendingTimers();
    });
    wrapper.update();
    expect(wrapper.find('StyledNotifyButton').prop('visible')).toBeTruthy();
  });
});

test result:

 PASS  examples/67440874/SomeComponent.test.tsx
  67440874
    ✓ should pass (8 ms)

  console.log
    showNotifyButton:  false

      at SomeComponent (examples/67440874/SomeComponent.tsx:13:11)

  console.log
    showNotifyButton:  true

      at SomeComponent (examples/67440874/SomeComponent.tsx:13:11)

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

package versions:

"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"jest": "^26.6.3",
"jest-environment-enzyme": "^7.1.2",
"jest-enzyme": "^7.1.2",
Related