假计时器不会在 Jest 中正确触发 setTimeout 调用

Fake timers don't trigger setTimeout call correctly in Jest

我有一个简单的 React UI-less 组件,它显示已传递操作的状态。 有 4 种状态 - 待定、运行、完成、失败。 这里的主要问题是 运行 和完成状态必须至少持续一段时间(例如 2 秒)。 我正在尝试使用 Jest (26.6.3) 及其假计时器测试此行为,但我无法使其正常工作。

组件代码:

export const TaskState = {
    PENDING: "pending",
    RUNNING: "running",
    FINISHED: "finished",
    FAILED: "failed",
};

export default function Task({ action, minDelay = 2000, children }) {
    const [state, setState] = useState(TaskState.PENDING);
    const [error, setError] = useState(null);
    const timeoutHandle = useRef(null);

    useEffect(() => {
        return () => {
            if (timeoutHandle.current) {
                clearTimeout(timeoutHandle.current);
            }
        };
    }, []);

    const resetState = () => {
        setState(TaskState.PENDING);
        setError(null);
    };

    const onSuccess = () => {
        setState(TaskState.FINISHED);
        timeoutHandle.current = setTimeout(resetState, minDelay);
    };

    const onError = errMsg => {
        setState(TaskState.FAILED);
        setError(errMsg);
    };

    const startAction = () => {
        setState(TaskState.RUNNING);
        const start = performance.now();
        let err = null;
        try {
            action();
        } catch (e) {
            err = e.message;
        } finally {
            const end = performance.now();
            const elapsedTime = end - start;
            const delayTime = minDelay - elapsedTime;
            if (elapsedTime < minDelay && minDelay > 0) {
                timeoutHandle.current = setTimeout(() => {
                    err ? onError(err) : onSuccess();
                }, delayTime);
            } else {
                err ? onError(err) : onSuccess();
            }
        }
    };

    return children({
        state,
        error,
        startAction,
    });
}
Task.propTypes = {
    action: PropTypes.func.isRequired,
    minDelay: PropTypes.number,
    children: PropTypes.func.isRequired,
};

有问题的测试用例:

// sample UI implementation of our UI-less Task component
const renderBasicButton = ({ state, error, startAction }) => {
    return (
        <button onClick={startAction}>
            <span data-testid="state">{state}</span>
            <span data-testid="error">{error}</span>
        </button>
    );
};

test("changes state to failed after delay when clicked and action throws error", () => {
    jest.useFakeTimers("modern");
    const minDelay = 1000;
    render(
        <Task
            minDelay={minDelay}
            action={() => {
                throw new Error("Test error");
            }}
        >
            {props => renderBasicButton(props)}
        </Task>
    );
    userEvent.click(screen.getByRole("button"));
    expect(screen.getByTestId("state")).toHaveTextContent(TaskState.RUNNING);
    expect(screen.getByTestId("error")).toHaveTextContent("");
    act(() => jest.advanceTimersByTime(minDelay * 2));
    screen.debug(); // state is still TaskState.RUNNING instead of TaskState.FAILED
    expect(screen.getByTestId("state")).toHaveTextContent(TaskState.FAILED);
    expect(screen.getByTestId("error")).toHaveTextContent("Test error");
    jest.useRealTimers();
});

调用动作回调,执行从 pending 到 运行 的状态转换,但不执行从 运行 failed 的下一个状态转换。 当我手动测试它时它有效。

最小示例 - https://codesandbox.io/s/heuristic-khorana-7vygv?file=/src/App.js

在全局 window 对象上正确调用 setTimeout 解决了这个问题。 这在实际应用程序中并不重要,但对于专门修补 window.setTimeout 和其他功能的 jest 假定时器很重要。