我创建了一个简单的倒计时组件,我想用Reaction测试库测试它是否正常工作.在浏览器中,一切都按预期工作,但在测试呈现的输出时,无论使用什么值,都不会前进超过一秒.

组件(基于Dan Abramovs blog post的自定义挂钩):

import React, { useEffect, useRef, useState } from "react"
import TimerButton from "../TimerButton/TimerButton"

// custom hook
function useInterval(callback, delay, state) {
  const savedCallback = useRef()

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback
  }, [callback])

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current()
    }
    if (state.isOn) {
      let id = setInterval(tick, delay)
      return () => clearInterval(id)
    }
  }, [state])
}

const Timer = () => {
  const initialState = {
    minutes: 25,
    seconds: 0,
    isOn: false,
  }
  const [timerData, setTimerData] = useState(initialState)

  useInterval(
    () => {
      if (timerData.seconds === 0) {
        if (timerData.minutes === 0) {
          setTimerData({ ...timerData, isOn: false })
        } else {
          setTimerData({
            ...timerData,
            minutes: timerData.minutes - 1,
            seconds: 59,
          })
        }
      } else {
        setTimerData({ ...timerData, seconds: timerData.seconds - 1 })
      }
    },
    1000,
    timerData,
  )

  const displayedTime = `${
    timerData.minutes >= 10 ? timerData.minutes : `0${timerData.minutes}`
  }:${timerData.seconds >= 10 ? timerData.seconds : `0${timerData.seconds}`}`

  const startTimer = () => {
    setTimerData({ ...timerData, isOn: true })
  }

  const stopTimer = () => {
    setTimerData({ ...timerData, isOn: false })
  }

  const resetTimer = () => {
    setTimerData(initialState)
  }

  return (
    <div className="timer__container" data-testid="timerContainer">
      <div className="timer__time-display" data-testid="timeDisplayContainer">
        {displayedTime}
      </div>
      <div className="timer__button-wrap">
        <TimerButton buttonAction={startTimer} buttonValue="Start" />
        <TimerButton buttonAction={stopTimer} buttonValue="Stop" />
        <TimerButton buttonAction={resetTimer} buttonValue="Reset" />
      </div>
    </div>
  )
}

export default Timer

和测试(上面分别有一个beforeEachafterEach调用jest.useFakeTimers()jest.useRealTimers()):

  it("Assert timer counts down by correct interval when starting timer", async () => {
    render(<Timer />)
    const initialTime = screen.getByText("25:00")
    const startTimerButton = screen.getByText("Start")

    expect(initialTime).toBeInTheDocument() // This works fine

    fireEvent.click(startTimerButton)
    act(() => {
      jest.advanceTimersByTime(5000)
    })

    await waitFor(() => expect(screen.getByText("24:55")).toBeInTheDocument())
  })

测试结果:

    Unable to find an element with the text: 24:55. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

    Ignored nodes: comments, script, style
    <body>
      <div>
        <div
          class="timer__container"
          data-testid="timerContainer"
        >
          <div
            class="timer__time-display"
            data-testid="timeDisplayContainer"
          >
            24:59 <--- This value is always the same 
          </div>
          <div
            class="timer__button-wrap"
          >
          { ... }
        </div>
      </div>
    </body>

我想断言,在以不同的增量将时间向前移动后,将显示正确的时间,如果我可以做到这一点,那么它将允许进行更全面的测试.

我try 过使用Jest中的许多计时方法,包括AdvanceTimersByTime()、runOnlyPendingTimers()、runAllTimers(),但都没有用,我永远也无法获得超过1秒的时间.我还在useInterval函数中的不同点添加了console.log(),并可以验证该函数是否被调用了不同的次数,这取决于我try 将时间提前多少(它被称为Numbers of Second+1次,正如预期的那样).

在注销状态数据时,我看到除了最后一次运行之外,timerData状态不会更新:

callback {"minutes":25,"seconds":0,"isOn":true} // Every time but the last

callback {"minutes":24,"seconds":59,"isOn":true} // The last time only

我认为这一定与我在更新期间如何引用状态值有关,或者与jest如何与状态更新交互假计时器有关,尽管目前我真的不知道.

推荐答案

所以,在try 了很多不同的方法之后,我有了解决这个问题的办法.

对调用useInterval()函数的进一步调查显示:

  • 达到了更新状态的正确行,但是在setInterval函数的所有这些循环之后,组件只重新呈现一次,尽管应该触发的状态更新次数(使用控制台日志(log)进行了验证)
  • 将传递给setInterval(i)的时间更改为小于500ms,表明时间提前了大约相当于1 % i
  • 该组件仅为setInterval函数的每个刻度在经过的时间为jest.advanceTimersByTime()之后重新呈现一次

由此可以看出,对于每个jest.advanceTimersByTime()的调用,只能移动setInterval函数的一次迭代,因此我发现将时间间隔向前移动正确时间量的唯一方法是通过助手函数重复调用AdvanceTimersByTime():

const advanceJestTimersByTime = (increment, iterations) => {
  for (let i = 0; i < iterations; i++) {
    act(() => {
      jest.advanceTimersByTime(increment)
    })
  }
}

测试已更新为:

it("Assert timer counts down by correct interval when starting timer", async () => {
  render(<Timer />)

  const initialTime = screen.getByText("25:00")
  const startTimerButton = screen.getByText("Start")

  expect(initialTime).toBeInTheDocument()

  fireEvent.click(startTimerButton)

  advanceJestTimersByTime(1000, 60)

  await waitFor(() => expect(screen.getByText("24:00")).toBeInTheDocument())
})

这将传递并允许在倒计时的任何时间点做出断言

Reactjs相关问答推荐

Redux Provider Stuck甚至彻底遵循了文档

有没有一种方法可以在没有强烈动画的情况下有条件地渲染组件?

如何使用useState响应快速/顺序状态更新

GitHub页面未正确显示Reaction应用程序网站

在继承值更改时重置react 挂钩窗体字段值

阻止组件更新的多分派Redux Thunk或setState是否有任何问题

使用Next.js和Next-intl重写区域设置

MUI 日期 Select 器 - 最小日期混乱

面临 React 模式中点击外部条件的问题

Spring Boot + React + MySQL应用的架构层面有哪些?

如何在使用 React Router v6 将当前页面的状态传递给上一页的同时导航到上一页?

条件渲染元素的测试不起作用

在 React 中,为什么在使用 addEventListener 时外部元素上的 onclick 事件监听器在内部元素的 onclick 事件监听器之前执行?

我正在通过 API(有效)从 MongoDB 传递数据.可视化但我不断收到此 TypeError: Cannot convert undefined or null to object

NextJS htaccess 设置

在 Spring Cloud Gateway 中运行的 React SPA 页面刷新出现白标错误

自动重新获取不起作用 - RTK 查询

如何不旋转 mui 自动完成弹出图标?

将 dict 值转换并插入到react 钩子的列表中

如何将 id 从页面传递到另一个页面?