1

I have a pretty basic React component that displays a sequence of dots for when something is loading, by default it displays up to 3 dots and the dots increment on an interval set within a componentDidMount function, resetting at the number of dots. The number of dots and interval can both be overwritten by passing through props. This component is below.

import * as React from 'react';

export interface Props {
    interval?: number;
    dots?: number;
}

class LoadingDots extends React.Component<Props, object> {
    public static defaultProps: Partial<Props> = {
        interval: 300,
        dots: 3
    };
    interval: any;

    state = {
        frame: 1,
        interval: this.props.interval,
        dots: this.props.dots
    }

    componentDidMount = () => {
        this.interval = setInterval(() => {
            this.setState({
                frame: this.state.frame + 1
            });
        }, this.props.interval);
    }

    componentWillUnmount() {
        clearInterval(this.interval);
    }

    public render() {
        let dots = this.state.frame % (this.props.dots + 1);
        let text = "";
        while (dots > 0) {
            text += ".";
            dots--;
        }
        return (
                <p className="loadingDots" {...this.props}>{text}</p>
        )
    }
}

export default LoadingDots;

The issue I'm now having is testing this component, following the Jest Documentation and Jest react testing: Check state after delay I have written the test below to check the correct default number of dots are rendered at the end of the set of frames (would pass with a 200ms delay).

test('LoadingDots by default renders with 3 dots', () => {
    var loadingDots = Enzyme.shallow(<LoadingDots interval={100} />);
    jest.useFakeTimers();
    jest.runAllTimers();
    setTimeout(() => {
        expect(loadingDots.find('p').text()).toBe("...")
    }, 300);
});

This test passes but shouldn't, it passes regardless of what value I put in the .ToBe of expect(loadingDots.find('p').text()).toBe("...").

I've tried rendering the component as var loadingDots = Enzyme.mount(); and var loadingDots = Enzyme.render(); but still the same outcome, does anyone see what's going wrong here? How can I get this to work correctly?

1 Answer 1

1

Issue 1

jest.useFakeTimers() replaces the timer functions like setInterval with timer mocks that track the arguments they were called with. It needs to be called before your component is created so the setInterval call in componentDidMount calls the mock instead of the real setInterval.

Issue 2

jest.runAllTimers() attempts to run all timer callbacks until there are none left. In this case setInterval is never cancelled so jest.runAllTimers would attempt to run timer callbacks forever and Jest would interrupt the test with an error after it runs 100000 callbacks.

The correct way to advance time in this scenario is to use jest.advanceTimersByTime (or the alias jest.runTimersToTime for Jest < 22).

Issue 3

setTimeout is not needed since Timer Mocks execute synchronously.


Here is an updated test for your component:

test('LoadingDots by default renders with 3 dots', () => {
  jest.useFakeTimers();  // replace timer functions like setInterval with timer mocks
  const dots = 3;
  const interval = 100;
  var loadingDots = Enzyme.shallow(<LoadingDots interval={interval} />);
  for(let i = 1; i <= 8; i++) {
    const expectedDots = i % (dots + 1);
    expect(loadingDots.find('p').first().text()).toBe('.'.repeat(expectedDots));
    jest.runTimersToTime(interval);  // simulate time passing and run any applicable timer callbacks
  }
  jest.useRealTimers();  // restore timer functions
});
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.