React测试触发beforeunload的自定义hook时卸载阶段监听移除失败问题
问题修复步骤
1. 先修复自定义hook的笔误
原hook存在两处变量名拼写错误,会直接导致逻辑异常:
- 入参名是
fns,但后续useEffect里引用的是未定义的fn,需要统一命名 - hook名建议统一为小驼峰(符合React自定义hook命名规范)
修正后的hook代码:
import { useRef, useEffect } from 'react'; const useBeforeUnload = (fn: (e: BeforeUnloadEvent) => void) => { const cb = useRef(fn); useEffect(() => { cb.current = fn; }, [fn]); useEffect(() => { const onUnloadFN = (args: BeforeUnloadEvent) => cb.current?.(args); window.addEventListener('beforeunload', onUnloadFN); return () => { window.removeEventListener('beforeunload', onUnloadFN); }; }, []); }; export default useBeforeUnload;
2. 修复测试用例问题
第二个测试失败的核心原因是:对window方法的mock方式不规范,存在跨用例的mock状态污染,且校验逻辑不严谨。你只校验了removeEventListener的事件名参数,没有统一管控每个用例的mock生命周期。
修正后的测试代码:
import { mount } from 'enzyme'; import React from 'react'; import useBeforeUnload from './useBeforeUnload'; // 把回调做成props传入,方便测试不同场景 const HookWrapper = ({ callback }: { callback: (e: BeforeUnloadEvent) => void }) => { useBeforeUnload(callback); return <div>component</div>; }; describe('useBeforeUnload', () => { // 每个用例执行前统一mock事件方法,避免跨用例污染 beforeEach(() => { jest.spyOn(window, 'addEventListener').mockImplementation(() => {}); jest.spyOn(window, 'removeEventListener').mockImplementation(() => {}); }); afterEach(() => { jest.restoreAllMocks(); }); it('should register beforeunload event listener on mount', () => { const mockCallback = jest.fn(); mount(<HookWrapper callback={mockCallback} />); expect(window.addEventListener).toHaveBeenCalledTimes(1); expect(window.addEventListener).toHaveBeenCalledWith( 'beforeunload', expect.any(Function) ); }); it('should remove beforeunload event listener on unmount', () => { const mockCallback = jest.fn(); const wrapper = mount(<HookWrapper callback={mockCallback} />); // 拿到注册时传入的回调函数,验证移除时传入的是同一个实例,符合浏览器事件移除规则 const registeredCallback = (window.addEventListener as jest.Mock).mock.calls[0][1]; wrapper.unmount(); expect(window.removeEventListener).toHaveBeenCalledTimes(1); expect(window.removeEventListener).toHaveBeenCalledWith( 'beforeunload', registeredCallback ); }); // 新增:验证刷新检测逻辑是否生效 it('should trigger callback when beforeunload event fires', () => { const mockCallback = jest.fn(); mount(<HookWrapper callback={mockCallback} />); const registeredCallback = (window.addEventListener as jest.Mock).mock.calls[0][1]; const mockEvent = new Event('beforeunload') as BeforeUnloadEvent; Object.defineProperty(mockEvent, 'returnValue', { value: '', writable: true }); // 模拟触发页面刷新/关闭的beforeunload事件 registeredCallback(mockEvent); expect(mockCallback).toHaveBeenCalledTimes(1); expect(mockCallback).toHaveBeenCalledWith(mockEvent); }); });
更优测试方案推荐
如果不想用enzyme包装组件测试hook,更推荐用专门的React hook测试库@testing-library/react-hooks,代码会更简洁,不需要额外包装组件:
import { renderHook } from '@testing-library/react-hooks'; import useBeforeUnload from './useBeforeUnload'; describe('useBeforeUnload', () => { beforeEach(() => { jest.spyOn(window, 'addEventListener').mockImplementation(() => {}); jest.spyOn(window, 'removeEventListener').mockImplementation(() => {}); }); afterEach(() => { jest.restoreAllMocks(); }); it('should register and remove listener correctly', () => { const mockCallback = jest.fn(); const { unmount } = renderHook(() => useBeforeUnload(mockCallback)); const registeredCallback = (window.addEventListener as jest.Mock).mock.calls[0][1]; expect(window.addEventListener).toHaveBeenCalledWith('beforeunload', registeredCallback); unmount(); expect(window.removeEventListener).toHaveBeenCalledWith('beforeunload', registeredCallback); }); });
内容的提问来源于stack exchange,提问作者Asking
相关产品推荐
相关产品推荐

