【问题标题】:How do you test temporary loading text with react testing library?您如何使用反应测试库测试临时加载文本?
【发布时间】:2021-10-28 08:51:47
【问题描述】:

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

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

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...'.

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

【问题讨论】:

  • doformStuff 需要异步才能工作,否则加载状态根本没有意义,因为成功或错误消息几乎会立即显示。

标签: reactjs forms testing react-testing-library


【解决方案1】:

问题是screen.getByTestId('title') 可能在测试生命周期内保存多个值。 对我有用的一种方法是waitFor 确切的瞬态值。 像这样的东西应该在你的例子中起作用:

async screen.findByText('Submitting...');

说实话,我不确定这是否是解决此问题的“正确”方法。我自己对这项技术很陌生。据我了解,findByTextis using polling,所以我怀疑这种方法可能会导致不稳定的结果。但它对我有用。

【讨论】:

    【解决方案2】:

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

    “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 在syncronous delay 上循环,直到锁被解决。由于每次清空调用堆栈时都会调用此验证,因此您可以随时确定地解锁它(就像我在“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...')
      })
    }
    

    【讨论】:

      猜你喜欢
      • 2021-05-09
      • 2022-06-21
      • 2020-01-21
      • 2021-06-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多