Skip to content
This repository has been archived by the owner on Jun 17, 2024. It is now read-only.

testing components using hooks and timeout #514

Open
dadamssg opened this issue Dec 8, 2018 · 7 comments
Open

testing components using hooks and timeout #514

dadamssg opened this issue Dec 8, 2018 · 7 comments
Labels
code-help StackOverflow-like questions testing

Comments

@dadamssg
Copy link

dadamssg commented Dec 8, 2018

I have a really simple slideshow class component i'm trying to convert to hooks to learn how to use and test them but failing. Was hoping you could point me in the right direction.

The class version's test pass while the hook version's test fails with:

Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.

I'm using setTimeout instead of setInterval because I want the user to able to advance to the next slide on click and have the 'interval' reset so that slide gets the full display time. I think I'm doing the recommended method of setting a state value to control when the effect is called.

If I remove jest's fake timers, the tests pass but i don't want the tests to wait the full interval time.

I'd create a codesandbox but codesandbox doesn't support jest.useFakeTimers().

Can you spot if i'm doing something wrong?

import React, {useEffect, useState, useRef} from 'react'
import {render, waitForElement} from 'react-testing-library'

jest.useFakeTimers()

class ClassSlideshow extends React.Component {
  state = {
    slides: [{id: 'abc'}, {id: 'def'}, {id: 'ghi'}],
    index: -1
  }
  componentDidMount () {
    this.advance()
  }
  componentWillUnmount () {
    clearTimeout(this.intervalId)
  }
  advance = () => {
    const {index, slides} = this.state
    const nextIndex = index + 1 === slides.length ? 0 : index + 1
    this.setState({index: nextIndex})
    clearTimeout(this.intervalId)
    this.intervalId = setTimeout(this.advance, 1000)
  }
  render () {
    const slide = this.state.slides[this.state.index]
    return (
      <button data-testid='slideshow' onClick={this.advance}>
        {slide ? slide.id : null}
      </button>
    )
  }
}

function HookSlideshow () {
  const intervalRef = useRef()
  const [index, setIndex] = useState(-1)
  const [advance, setAdvance] = useState(true)
  const [slides] = useState([{id: 'abc'}, {id: 'def'}, {id: 'ghi'}])

  // clean up on unmount
  useEffect(() => () => clearTimeout(intervalRef.current), [])

  useEffect(
    () => {
      if (!advance) return
      const nextIndex = index + 1 === slides.length ? 0 : index + 1
      setIndex(nextIndex)
      clearTimeout(intervalRef.current)
      intervalRef.current = setTimeout(() => {
        setAdvance(true)
      }, 1000)
      setAdvance(false)
    },
    [advance]
  )
  const slide = slides[index]
  return (
    <button data-testid='slideshow' onClick={() => setAdvance(true)}>
      {slide ? slide.id : null}
    </button>
  )
}

describe('Slideshow', () => {
  test('class slideshow', async () => {
    const {getByText} = render(<ClassSlideshow />)
    await waitForElement(() => getByText('abc'))
    jest.advanceTimersByTime(1000)
    await waitForElement(() => getByText('def'))
    jest.advanceTimersByTime(1000)
    await waitForElement(() => getByText('ghi'))
    jest.advanceTimersByTime(1000)
    await waitForElement(() => getByText('abc'))
  })
  test('hook slideshow', async () => {
    const {getByText} = render(<HookSlideshow />)
    await waitForElement(() => getByText('abc'))
    jest.advanceTimersByTime(1000)
    await waitForElement(() => getByText('def'))
    jest.advanceTimersByTime(1000)
    await waitForElement(() => getByText('ghi'))
    jest.advanceTimersByTime(1000)
    await waitForElement(() => getByText('abc'))
  })
})
@hedgerh hedgerh added code-help StackOverflow-like questions testing labels Dec 9, 2018
@kentcdodds
Copy link
Owner

Hi @dadamssg! I haven't taken the time to look at this closely yet, but I thought I'd mention that you'd probably be more likely to get an answer quicker on spectrum.

I'll look at it as soon as I'm able.

@kentcdodds
Copy link
Owner

Hi @dadamssg,

Did you get this question answered?

@kentcdodds
Copy link
Owner

I'm pretty sure I saw that this was resolved elsewhere.

@erezrokah
Copy link

erezrokah commented Feb 23, 2019

@kentcdodds kentcdodds reopened this Feb 23, 2019
@erezrokah
Copy link

Hey @kentcdodds I found a workaround.
After debugging my test I noticed waitForElement uses setTimeout internally, which of course doesn't work when using jest fake timers:
https://github.com/kentcdodds/dom-testing-library/blob/4c17512e77f16b3aa1c87c946bfc6c5320c29193/src/wait-for-element.js#L21

The solution is to run jest.useRealTimers() before calling waitForElement:
https://github.com/erezrokah/serverless-monitoring-app/blob/42309605facfcc54edbc3e0214a582508360dae4/frontend/src/components/DataTable.test.tsx#L79

@mamachanko
Copy link

I think I was able to produce a simple yet complete demo of continuous, asynchronous polling using @erezrokah 's workaround:

https://github.com/mamachanko/continuous-asynchronous-polling-with-react-hooks

@gzaripov
Copy link

I can still reproduce this issue on [email protected]

This test fails with timeout error, the same with waitForElement():

act(() => jest.runOnlyPendingTimers());
await waitForDomChange();

But if I use jest.useRealTimers() it works as expected:

act(() => jest.runOnlyPendingTimers());
jest.useRealTimers();
await waitForDomChange();

Also lets to move this issue to dom-testing-library repository because it is the first place where people will search.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
code-help StackOverflow-like questions testing
Projects
None yet
Development

No branches or pull requests

6 participants