假计时器不会在 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 假定时器很重要。
我有一个简单的 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 假定时器很重要。