You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 02:57:03