如何用react测试库测试临时加载文本?



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

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

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

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

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

"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",然后向它发送一个带有locks值的函数。delayWhile循环一个同步延迟,直到锁被解析。由于每次清空调用堆栈时都会调用此验证,因此您可以随时确定地解锁它(就像我在applyChangeOfState&quot上所做的那样)。因为一旦调用堆栈为空,就会调用这个函数,如果要直接计算结果,可能会因为还没有应用更改而失败,所以应该将期望封装在waitFor中或使用findBy反应查询中。最后,afterEach位确保我们解锁它,以便它总是能够整齐地退出循环,而不是超时。

如何将其应用于您的用例

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

您可能想在测试中模拟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...')
})
}

问题是screen.getByTestId('title')可能在整个测试生命周期中保持多个值。一种对我有效的方法是waitFor精确的瞬态值。在您的示例中应该这样做:

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

老实说,我不确定这是否"正确"。解决这个问题的方法。我自己对这项技术也很陌生。据我所知,findByText正在使用轮询,所以我怀疑这种方法可能会导致不稳定的结果。但它对我有效。

最新更新