无法使用jest.useFakeTimers的情况: setTimeout中出现了ref.current,且使用这个ref的组件还未渲染
具体的业务场景是写了一个按钮,在点击之后Modal弹窗会显示,500ms后弹窗中的输入框会获得焦点
ts
onClick={() => {
open();
setTimeout(() => {
ref.current.focus();
}, 500);
}}
如上代码所示,当你打算在setTimeout中访问ref.current时,且如果使用了如下代码
js
// 指定 Jest 使用假的全局日期、性能、时间和定时器 API
beforeEach(() => {
jest.useFakeTimers()
});
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
});
user.click(button)就会超时,此时,如果你使用waitFor将其包裹,则会看到真正的报错
js
await waitFor(() => user.click(button));
javascript
TypeError: Cannot read properties of null (reading 'focus')
36 | setTimeout(() => {
> 37 | ref.current.focus();
| ^
38 | }, 500);
39 | }}
根因是因为jest.useFakeTimers运行了之后,setTimeout会立即执行,不会等待,而这时的ref.current还未被赋值。
React提供的act函数可以在包裹一段代码后,可以等待react组件渲染完成。
一般我们可以把它包在render函数之外,如下代码所示:
js
it ('renders with button disabled', async () => {
await act(async () => {
root.render(<TestComponent />)
});
expect(container.querySelector('button')).toBeDisabled();
});
这样即使TestComponent中使用了useRef,对应的ref也能获取到值。
但由于我们测试的是modal组件,需要点击之后才显示,所以无法使用这个方案。
所以解决方案就是,删除jest.useFakeTimers相关代码,让setTimeout异步执行,这样ref才能在click后有时间获取到值。由于此时需要等待500ms,所以我们也需要setTimeout,由于此时react组件会更新,所以我们需要用act包裹,所以最终的解决方案就是
tsx
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
it('点击按钮后,出现弹窗且输入框获得焦点', async () => {
const user = userEvent.setup();
render(
<Modal>
<Button>创建</Button>
</Modal>
);
const button = screen.getByText('创建');
await user.click(button);
await act(async () => await sleep(500)); // 等待ref获得值并focus
expect(screen.getByPlaceholderText('请输入')).toHaveFocus();
});