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
相关产品推荐
相关产品推荐

