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

React组件测试问题:space按键时e.preventDefault未被调用

问题:React SoundBtn组件的空格键测试用例失败,preventDefaultSpy未被调用

我在React项目中开发了一个切换声音开关的SoundBtn组件,组件内的handleKeyDown函数逻辑是按下空格键时调用e.preventDefault(),避免触发togglePlay功能。但对应的测试用例执行失败,报错显示preventDefaultSpy从未被调用,无法排查原因,寻求帮助。

组件代码

import { useAppSelector, useAppDispatch } from '../../../../app/hooks';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faVolumeXmark} from '@fortawesome/free-solid-svg-icons';
import { faVolumeHigh} from '@fortawesome/free-solid-svg-icons';
import { RootState } from '../../../../app/store';
import { toggleMute } from '../../../../features/soundSlice/soundSlice';
import './SoundBtn.css';

export const SoundBtn = () => {
  const isMuted = useAppSelector((state: RootState) => state.sound.isMuted);
  const dispatch = useAppDispatch();

  // Prevent space key from triggering togglePlay
  const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
    if (e.code === ' ' || e.key === 'Space') {
      e.preventDefault(); 
    }
  };

  const togglePlay = () => {
    dispatch(toggleMute());
  };

  return (
    <button 
      className="commandBtn nonHoverBtn" 
      id='soundBtn' 
      onClick={togglePlay}
      onKeyDown={handleKeyDown}
    >
      {!isMuted ? (
        <FontAwesomeIcon 
          icon={faVolumeHigh} 
          className='soundBtnIcon'
          aria-label='click to mute sound'
        />
      ) : (
        <FontAwesomeIcon 
          icon={faVolumeXmark} 
          className='soundBtnIcon'
          aria-label='click to play sound'
        />
      )}
    </button>
  )
};

测试代码

import { fireEvent, screen } from "@testing-library/react";
import { renderWithProviders } from "../../test-utils";
import { SoundBtn } from "../../../components/ui/buttons/soundBtn/SoundBtn";

describe('SoundBtn component', () => {
  it('renders the component and the correct button when muted without crashing', () => {
    const preloadedState = {
      sound: {
        isMuted: true, 
      },
    }; 

    renderWithProviders(<SoundBtn />, { preloadedState });

    expect(screen.getByLabelText('click to play sound')).toBeInTheDocument();
  });

  it('renders the component and the correct button when not muted without crashing', () => {
    const preloadedState = {
      sound: {
        isMuted: false, 
      },
    };

    renderWithProviders(<SoundBtn />, { preloadedState });

    expect(screen.getByLabelText('click to mute sound')).toBeInTheDocument();
  });

  it('toggles mute, when button is clicked', () => {
    const preloadedState = {
      sound: {
        isMuted: true, 
      },
    };
        
    const { store } = renderWithProviders(<SoundBtn />, { preloadedState });
    expect(store.getState().sound.isMuted).toBe(true);

    fireEvent.click(screen.getByLabelText('click to play sound'));

    const updatedState = store.getState().sound; 
    expect(updatedState.isMuted).toBe(false); 

    fireEvent.click(screen.getByLabelText('click to mute sound'));

    const newUpdatedState = store.getState().sound; 
    expect(newUpdatedState.isMuted).toBe(true); 
  });

  it('does not toggle sound, when space key is clicked', () => {
    const preloadedState = {
      sound: {
        isMuted: true, 
      },
    };
    const { getByLabelText } = renderWithProviders(<SoundBtn />);
    const button = getByLabelText('click to play sound');

    const preventDefaultSpy = vi.fn();

    const spaceEvent = new KeyboardEvent('keydown', { code: 'Space', key: ' ' });
    fireEvent.keyDown(button , spaceEvent);

    expect(preventDefaultSpy).toHaveBeenCalled();
    const { store } = renderWithProviders(<SoundBtn />, { preloadedState });
    expect(store.getState().sound.isMuted).toBe(true);
  });
});

错误信息

FAIL src/tests/components/ui/soundBtn.test.tsx > SoundBtn component > does not toggle sound, when space key is clicked
AssertionError: expected "spy" to be called at least once
❯ src/tests/components/ui/soundBtn.test.tsx:68:35
66| fireEvent.keyDown(button , spaceEvent);
67|
68| expect(preventDefaultSpy).toHaveBeenCalled();

RERUN src/tests/components/ui/soundBtn.test.tsx x149


问题分析与修复

1. 核心问题点

  • 键盘事件判断逻辑错误:e.code的有效值是'Space',e.key的有效值是' ',组件内写反了,导致条件不成立,根本不会执行e.preventDefault()。
  • Spy未绑定到事件对象:创建了preventDefaultSpy但没挂载到键盘事件的preventDefault方法上,组件调用时不会触发这个spy。
  • 重复渲染组件:测试末尾再次调用renderWithProviders,会生成全新的组件实例,无法验证之前触发键盘事件的组件状态。

2. 分步修复

第一步:修正组件内的handleKeyDown逻辑

调换e.code和e.key的判断值:

const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
  // 修正:e.code为'Space',e.key为' '
  if (e.code === 'Space' || e.key === ' ') {
    e.preventDefault(); 
  }
};

第二步:修复测试用例

将spy绑定到事件对象,并且不再重复渲染组件:

it('does not toggle sound when space key is pressed', () => {
  const preloadedState = {
    sound: {
      isMuted: true, 
    },
  };
  // 一次性渲染并获取store,确保状态一致
  const { getByLabelText, store } = renderWithProviders(<SoundBtn />, { preloadedState });
  const button = getByLabelText('click to play sound');

  const preventDefaultSpy = vi.fn();
  // 创建可取消的键盘事件
  const spaceEvent = new KeyboardEvent('keydown', { 
    code: 'Space', 
    key: ' ',
    cancelable: true 
  });
  // 将spy绑定到事件的preventDefault方法
  Object.defineProperty(spaceEvent, 'preventDefault', {
    value: preventDefaultSpy
  });

  fireEvent.keyDown(button, spaceEvent);

  // 验证spy被调用
  expect(preventDefaultSpy).toHaveBeenCalled();
  // 验证状态未变化
  expect(store.getState().sound.isMuted).toBe(true);
});

3. 额外说明

  • 必须给键盘事件设置cancelable: true,否则preventDefault()无法生效。
  • 测试过程中不要重复渲染组件,否则会丢失之前的组件实例和状态,导致测试结果失真。

内容的提问来源于stack exchange,提问作者Mawin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 23:12:07