4

I was wondering how to test what does interval dependant timer display after a few ticks with React Testing Library and Jest. Assume we have such code:

import React, { Component } from 'react';

let timer;

class Test extends Component {
    constructor() {
        super();
        this.state = {
            timeLeft: 60
        }
    }

    componentDidMount() {
        this.handleTick();
    }

    componentDidUpdate() {
        const { timeLeft } = this.state;
        if (!timeLeft) {
            clearInterval(timer);
            this.setState({
                timeLeft: 60,
            })
        }
    }

    handleTick() {
        timer = setInterval(() => {
            this.setState({timeLeft: this.state.timeLeft - 1 })
        },1000)
    }

    render() {
        return (
            <React.Fragment>
                <h3>I'm test.</h3>
                <h4>{this.state.timeLeft}</h4>
            </React.Fragment>
        )
    }
}

And now in test we want to check if Test component shows exactly what we want after let's say 15 sec. I have tried:

describe('Test Component', () => {
  test('Timer should display 45 sec left', () => {
    jest.useFakeTimers();
    const { getByText } = render(<Test />);
    setTimeout(() => {
      expect(getByText('45')).toBeInTheDocument();
    }, 15000);
    jest.runAllTimers();
  });
});

It pass the test, but if we change code line from

expect(getByText('45')).toBeInTheDocument();

to

expect(getByText('55')).toBeInTheDocument();

it passes to... So it seams that it doesn't work as I was expected. Do you have any ideas how to write this test properly? Of course I don't want to delay my tests.

NearHuscarl
  • 66,950
  • 18
  • 261
  • 230

1 Answers1

7

You can use jest.advanceTimersByTime(num) to advance by num milliseconds to the future. Remember to wrap the code above in act() if the component state is updated in that time so React can update the state properly before the assertion.

test('Timer should display 45 sec left', () => {
  jest.useFakeTimers();
  const { getByText } = render(<Test />);

  act(() => {
    jest.advanceTimersByTime(1500);
  })
  expect(getByText('45')).toBeInTheDocument();
})
NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
  • Thank you very much, that worked! Now I'm trying to do this same in my real-world application where I'm using third part library ```react-countdown-circle-timer```to display a CountdownCircleTimer and remainingTime. It dosent work but it seems also that my timer dosen't tick in test env, and this is propably another think to figure out. Anyway, thank you for your time :) – Bartłomiej Tuchowski Oct 17 '20 at 14:46
  • It looks like `react-countdown-circle-timer` uses [`requestAnimationFrame`](https://github.com/vydimitrov/use-elapsed-time/blob/c9fd8db451b03c0ad543d7a2543ba5b61a90f42d/src/hooks/useElapsedTime.js#L25) under the hood, not `setTimeout` like in your question. See my other answer [here](https://stackoverflow.com/a/64336022/9449426) to setup fake timers using modern implementation that not just mocks `setTimeout`. Hopefully it will solve your problem. @BartłomiejTuchowski – NearHuscarl Oct 17 '20 at 15:08