首页 > 解决方案 > 您如何使用反应测试库测试临时加载文本?

问题描述

我有一个表格,在处理时显示“提交”状态,完成后显示“已提交”状态。

这是我正在使用的提交处理程序...

const handleSubmit = (e, _setTitle) => {
    e.preventDefault()
    _setTitle('Submitting...')
    try {
      doformStuff(emailRef.current.value)
    } catch (err) {
      throw new Error(err)
    } finally {
      _setTitle('Submitted perfectly.')
    }
  }

我想测试提交状态是否出现,无论多么简短。

it('shows "submitting" when submitting', async () => {
    // arrange
    render(<MobileEmailCapture/>)
    // act
    const emailInput = screen.getByPlaceholderText('yourem@il.com')
    userEvent.type(emailInput, fakeEmail)
    fireEvent.submit(screen.getByTestId('form'))
    // assert
    expect(screen.getByTestId('title')).toHaveTextContent('Submitting...')
  })

问题是测试直接跳转到提交状态 Error: FAILED Expected 'Submitted perfectly.' to match 'Submitting...'.

我知道这就是它结束的地方,但我想测试临时过渡状态。我怎么做?

标签: reactjsformstestingreact-testing-library

解决方案


我想出了一个解决这个问题的方法,尽管它确实需要一些解释。我将从一些原始但抽象的代码开始,并在将其与您的特定问题相关联时对其进行解释。

“delayWhile”解决方案

import {waitFor} from "@testing-library/react"

describe('Button should', () => {
  let sideEffect = NO_SIDE_EFFECT
  let lock = {locked: true}

  beforeEach(() => {
    sideEffect = NO_SIDE_EFFECT
  })

  afterEach(() => {
    lock.locked = false
  })

  it("Side effect not applied", async () => {
    changeStateLater()
    await waitFor(() => expect(sideEffect).toBe(NO_SIDE_EFFECT))
  })

  it("Side effect applied", async () => {
    changeStateLater()
    applyChangeOfState()
    await waitFor(() => expect(sideEffect).toBe(SIDE_EFFECTED))
  })

  async function changeStateLater() {
    await delayWhile(() => lock.locked)
    sideEffect = SIDE_EFFECTED
  }

  function applyChangeOfState() {
    lock.locked = false
  }
})

async function delayWhile(condition: () => boolean, timeout = 2000) {
  const started = performance.now();
  while (condition()) {
    const elapsed = performance.now() - started;
    if (elapsed > timeout) throw Error("delayWhile lock was never released");
    await syncronousDelay();
  }
}

function syncronousDelay (n: number = 0) {
  return new Promise((done: any) => {
    setTimeout(() => { done() }, n)
  })
}
const NO_SIDE_EFFECT = "no side effect"
const SIDE_EFFECTED = "side effected"

好的,这可能比我需要显示的代码更多。我想给你一个完整的工作示例,这样,如果你和我一样需要看到一些工作并弄乱它以理解它,你可以自己测试它。

这里的想法是我们生成一个异步锁,并且总是在 beforeEach 内部生成一个新的锁定锁。“changeStateLater”代表您想要在完成之前调用和测试的任何异步调用(在您的情况下为“doformStuff”)。这个函数会调用“delayWhile”,然后你给它发送一个带有锁值的函数。delayWhile 在同步延迟上循环直到锁被解决。由于每次清空调用堆栈时都会调用此验证,因此您可以随时确定地解锁它(就像我在“applyChangeOfState”上所做的那样)。由于这将在调用堆栈为空时调用,如果您要直接评估结果,则可能由于尚未应用更改而失败,因此您应该将期望包装在 waitFor 或使用 findBy react 查询。最后,afterEach 位确保我们将其解锁,以便它始终可以整齐地退出循环,而不是超时。

如何将其应用于您的用例

首先,doformStuff 需要是异步的,你需要在handleSubmit 中等待它。否则,您将永远不会显示中间状态。

您可能想在测试中模拟 doformStuff 并在其中使用我的异步锁“delayWhile”。一个简单的方法是让该函数成为渲染组件的依赖项,但如果这不可行,您也可以将该函数移动到一个单独的文件中并从那里调用它。在测试中,您可以执行以下操作:

import * as foo from "@/whatever_your_file_is";

describe("Bar", () => {
  const doformStuffSpy = jest.spyOn(foo, "doformStuff")
  let api_lock = { locked: true }

  beforeEach(() => {
    // refresh the lock object
    api_lock = { locked: true }
    doformStuffSpy.mockReset()
    doformStuffSpy.mockReturnValue(Promise.resolve())
  });

  afterEach(() => {
    // clear the lock so that it does not keep polling until timeout
    api_lock.locked = false
  })

  it('shows "submitting" when submitting', async () => {
    // arrange
    render(<MobileEmailCapture/>)
    // act
    const emailInput = screen.getByPlaceholderText('yourem@il.com')
    userEvent.type(emailInput, fakeEmail)
    // this will lock doformStuff and not let it return until we say so
    doformStuffSpy.mockImplementation(() => delayWhile(() => api_lock.locked))
    fireEvent.submit(screen.getByTestId('form'))
    // assert
    expect(screen.getByTestId('title')).toHaveTextContent('Submitting...')
  })

  function call_this_if_you_want_to_unlock_it() {
    api_lock.locked = false
  }
}

async function delayWhile(condition: () => boolean, timeout = 2000) {
  const started = performance.now();
  while (condition()) {
    const elapsed = performance.now() - started;
    if (elapsed > timeout) throw Error("delayWhile lock was never released");
    await syncronousDelay();
  }
}

function syncronousDelay (n: number = 0) {
  return new Promise((done: any) => {
    setTimeout(() => { done() }, n)
  })
}

我希望这有点清楚,如果不是,请告诉我,我很乐意解决您可能遇到的任何问题。

奖金简单版

如果你想要一个更简单但更脆弱的实现,你可以在模拟函数中添加一些同步延迟。这将使您免于使用 delayWhile。只需替换前面示例中的以下行并删除 api_lock:

//doformStuffSpy.mockImplementation(() => delayWhile(() => api_lock.locked))
doformStuffSpy.mockReturnValue(new Promise(resolve => setTimeout(resolve, 2000)))

当然,这具有不确定性的问题。尽管 2 秒(或任何类似的值)对于您必须运行的测试逻辑来说应该足够了,但感觉有点奇怪。如果“delayWhile”解决方案对于您的用例来说似乎有点过于庞大,您仍然可以使用它。

最终结果将类似于:

import * as foo from "@/whatever_your_file_is";

describe("Bar", () => {
  const doformStuffSpy = jest.spyOn(foo, "doformStuff")

  beforeEach(() => {
    doformStuffSpy.mockReset()
    doformStuffSpy.mockReturnValue(Promise.resolve())
  });

  it('shows "submitting" when submitting', async () => {
    // arrange
    render(<MobileEmailCapture/>)
    // act
    const emailInput = screen.getByPlaceholderText('yourem@il.com')
    userEvent.type(emailInput, fakeEmail)
    // this will lock doformStuff for 2 seconds, should be enough to make our assertions
    doformStuffSpy.mockReturnValue(new Promise(resolve => setTimeout(resolve, 2000)))
    fireEvent.submit(screen.getByTestId('form'))
    // assert
    expect(screen.getByTestId('title')).toHaveTextContent('Submitting...')
  })
}

推荐阅读