Không dùng setTimeout thật để chờ — dùng fake timer và tự đẩy đồng hồ.
beforeEach(() => vi.useFakeTimers())
afterEach(() => vi.useRealTimers())
it("calls the API once after the user stops typing", async () => {
// user-event must share the fake clock, otherwise it hangs
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
render(<SearchBox onSearch={onSearch} />)
await user.type(screen.getByRole("searchbox"), "react")
expect(onSearch).not.toHaveBeenCalled()
await act(async () => { vi.advanceTimersByTime(300) })
expect(onSearch).toHaveBeenCalledTimes(1)
expect(onSearch).toHaveBeenCalledWith("react")
})Vì sao hay treo: user-event v14 tự chèn delay giữa các phím bằng setTimeout. Khi fake timer đang bật mà không ai đẩy đồng hồ, timer đó không bao giờ chạy → await user.type(...) treo tới khi test timeout. Truyền advanceTimers cho setup() để nó tự đẩy.
Các điểm khác cần nhớ:
- Bọc advanceTimersByTime trong act khi việc đẩy thời gian gây setState, nếu không React cảnh báo và DOM chưa cập nhật.
- Luôn useRealTimers() ở afterEach, nếu không fake clock rò sang test sau.
- Jest tương đương: jest.useFakeTimers() + jest.advanceTimersByTime(300); muốn chạy hết mọi timer đang chờ thì runAllTimers, nhưng với code có setInterval thì runAllTimers sẽ lặp vô hạn — dùng advanceTimersByTime cho an toàn.