Enzyme 交互模拟 #
交互模拟概述 #
Enzyme 提供了 simulate 方法来模拟用户交互事件,让测试可以验证组件对用户操作的响应。
simulate 方法 #
javascript
wrapper.find(selector).simulate(eventName, [eventData]);
支持的事件类型 #
text
┌─────────────────────────────────────────────────────────────┐
│ 支持的事件类型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 鼠标事件 │
│ ├── click 单击 │
│ ├── doubleclick 双击 │
│ ├── mousedown 鼠标按下 │
│ ├── mouseup 鼠标释放 │
│ ├── mouseover 鼠标移入 │
│ ├── mouseout 鼠标移出 │
│ └── mouseenter 鼠标进入 │
│ │
│ 表单事件 │
│ ├── change 值改变 │
│ ├── input 输入 │
│ ├── submit 表单提交 │
│ ├── focus 获得焦点 │
│ ├── blur 失去焦点 │
│ └── select 选择 │
│ │
│ 键盘事件 │
│ ├── keydown 按键按下 │
│ ├── keyup 按键释放 │
│ └── keypress 按键按下(已废弃) │
│ │
│ 其他事件 │
│ ├── copy 复制 │
│ ├── cut 剪切 │
│ ├── paste 粘贴 │
│ ├── scroll 滚动 │
│ └── contextmenu 右键菜单 │
│ │
└─────────────────────────────────────────────────────────────┘
点击事件 #
基本点击 #
javascript
function Button({ onClick, text }) {
return (
<button onClick={onClick}>{text}</button>
);
}
describe('Button click', () => {
it('handles click event', () => {
const mockClick = jest.fn();
const wrapper = shallow(<Button onClick={mockClick} text="Click me" />);
wrapper.find('button').simulate('click');
expect(mockClick).toHaveBeenCalledTimes(1);
});
it('handles click with event data', () => {
const mockClick = jest.fn();
const wrapper = shallow(<Button onClick={mockClick} text="Click" />);
wrapper.find('button').simulate('click', {
target: { value: 'test' }
});
expect(mockClick).toHaveBeenCalledWith(
expect.objectContaining({ target: { value: 'test' } })
);
});
});
点击计数器 #
javascript
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span className="count">{count}</span>
<button
className="increment"
onClick={() => setCount(count + 1)}
>
+
</button>
<button
className="decrement"
onClick={() => setCount(count - 1)}
>
-
</button>
</div>
);
}
describe('Counter', () => {
it('increments count', () => {
const wrapper = shallow(<Counter />);
wrapper.find('.increment').simulate('click');
expect(wrapper.find('.count').text()).toBe('1');
});
it('decrements count', () => {
const wrapper = shallow(<Counter />);
wrapper.find('.decrement').simulate('click');
expect(wrapper.find('.count').text()).toBe('-1');
});
it('handles multiple clicks', () => {
const wrapper = shallow(<Counter />);
wrapper.find('.increment').simulate('click');
wrapper.find('.increment').simulate('click');
wrapper.find('.increment').simulate('click');
expect(wrapper.find('.count').text()).toBe('3');
});
});
阻止默认行为 #
javascript
function Link({ href, onClick }) {
return (
<a
href={href}
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
Click me
</a>
);
}
describe('Link', () => {
it('prevents default and calls handler', () => {
const mockClick = jest.fn();
const wrapper = shallow(<Link href="/test" onClick={mockClick} />);
const mockEvent = { preventDefault: jest.fn() };
wrapper.find('a').simulate('click', mockEvent);
expect(mockEvent.preventDefault).toHaveBeenCalled();
expect(mockClick).toHaveBeenCalled();
});
});
表单事件 #
输入变化 #
javascript
function SearchInput({ onSearch }) {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
};
const handleSubmit = () => {
onSearch(value);
};
return (
<div>
<input
value={value}
onChange={handleChange}
placeholder="Search..."
/>
<button onClick={handleSubmit}>Search</button>
</div>
);
}
describe('SearchInput', () => {
it('updates value on change', () => {
const wrapper = shallow(<SearchInput onSearch={jest.fn()} />);
wrapper.find('input').simulate('change', {
target: { value: 'test query' }
});
expect(wrapper.find('input').prop('value')).toBe('test query');
});
it('calls onSearch with input value', () => {
const mockSearch = jest.fn();
const wrapper = shallow(<SearchInput onSearch={mockSearch} />);
wrapper.find('input').simulate('change', {
target: { value: 'test query' }
});
wrapper.find('button').simulate('click');
expect(mockSearch).toHaveBeenCalledWith('test query');
});
});
表单提交 #
javascript
function LoginForm({ onSubmit }) {
const [formData, setFormData] = useState({ username: '', password: '' });
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={formData.username}
onChange={handleChange}
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
/>
<button type="submit">Login</button>
</form>
);
}
describe('LoginForm', () => {
it('submits form data', () => {
const mockSubmit = jest.fn();
const wrapper = shallow(<LoginForm onSubmit={mockSubmit} />);
// 填写用户名
wrapper.find('input[name="username"]').simulate('change', {
target: { name: 'username', value: 'testuser' }
});
// 填写密码
wrapper.find('input[name="password"]').simulate('change', {
target: { name: 'password', value: 'testpass' }
});
// 提交表单
wrapper.find('form').simulate('submit', { preventDefault: jest.fn() });
expect(mockSubmit).toHaveBeenCalledWith({
username: 'testuser',
password: 'testpass'
});
});
});
复选框和单选框 #
javascript
function Preferences({ onChange }) {
const [preferences, setPreferences] = useState({
newsletter: false,
notifications: true,
theme: 'light'
});
const handleCheckbox = (e) => {
const newPrefs = {
...preferences,
[e.target.name]: e.target.checked
};
setPreferences(newPrefs);
onChange(newPrefs);
};
const handleRadio = (e) => {
const newPrefs = {
...preferences,
theme: e.target.value
};
setPreferences(newPrefs);
onChange(newPrefs);
};
return (
<div>
<label>
<input
type="checkbox"
name="newsletter"
checked={preferences.newsletter}
onChange={handleCheckbox}
/>
Newsletter
</label>
<label>
<input
type="checkbox"
name="notifications"
checked={preferences.notifications}
onChange={handleCheckbox}
/>
Notifications
</label>
<div>
<label>
<input
type="radio"
name="theme"
value="light"
checked={preferences.theme === 'light'}
onChange={handleRadio}
/>
Light
</label>
<label>
<input
type="radio"
name="theme"
value="dark"
checked={preferences.theme === 'dark'}
onChange={handleRadio}
/>
Dark
</label>
</div>
</div>
);
}
describe('Preferences', () => {
it('handles checkbox change', () => {
const mockChange = jest.fn();
const wrapper = shallow(<Preferences onChange={mockChange} />);
wrapper.find('input[name="newsletter"]').simulate('change', {
target: { name: 'newsletter', checked: true }
});
expect(mockChange).toHaveBeenCalledWith(
expect.objectContaining({ newsletter: true })
);
});
it('handles radio change', () => {
const mockChange = jest.fn();
const wrapper = shallow(<Preferences onChange={mockChange} />);
wrapper.find('input[value="dark"]').simulate('change', {
target: { name: 'theme', value: 'dark' }
});
expect(mockChange).toHaveBeenCalledWith(
expect.objectContaining({ theme: 'dark' })
);
});
});
下拉选择 #
javascript
function CountrySelect({ onChange }) {
const [country, setCountry] = useState('');
const handleChange = (e) => {
setCountry(e.target.value);
onChange(e.target.value);
};
return (
<select value={country} onChange={handleChange}>
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="cn">China</option>
</select>
);
}
describe('CountrySelect', () => {
it('handles selection change', () => {
const mockChange = jest.fn();
const wrapper = shallow(<CountrySelect onChange={mockChange} />);
wrapper.find('select').simulate('change', {
target: { value: 'cn' }
});
expect(mockChange).toHaveBeenCalledWith('cn');
expect(wrapper.find('select').prop('value')).toBe('cn');
});
});
键盘事件 #
基本键盘事件 #
javascript
function InputWithEnter({ onSubmit }) {
const [value, setValue] = useState('');
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
onSubmit(value);
}
};
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
/>
);
}
describe('InputWithEnter', () => {
it('submits on Enter key', () => {
const mockSubmit = jest.fn();
const wrapper = shallow(<InputWithEnter onSubmit={mockSubmit} />);
// 输入值
wrapper.find('input').simulate('change', {
target: { value: 'test' }
});
// 按 Enter
wrapper.find('input').simulate('keydown', { key: 'Enter' });
expect(mockSubmit).toHaveBeenCalledWith('test');
});
it('does not submit on other keys', () => {
const mockSubmit = jest.fn();
const wrapper = shallow(<InputWithEnter onSubmit={mockSubmit} />);
wrapper.find('input').simulate('change', {
target: { value: 'test' }
});
wrapper.find('input').simulate('keydown', { key: 'Escape' });
expect(mockSubmit).not.toHaveBeenCalled();
});
});
快捷键组合 #
javascript
function ShortcutHandler({ onShortcut }) {
const handleKeyDown = (e) => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
onShortcut('save');
}
if (e.ctrlKey && e.key === 'z') {
e.preventDefault();
onShortcut('undo');
}
};
return <div tabIndex={0} onKeyDown={handleKeyDown} />;
}
describe('ShortcutHandler', () => {
it('handles Ctrl+S', () => {
const mockShortcut = jest.fn();
const wrapper = shallow(<ShortcutHandler onShortcut={mockShortcut} />);
wrapper.find('div').simulate('keydown', {
ctrlKey: true,
key: 's',
preventDefault: jest.fn()
});
expect(mockShortcut).toHaveBeenCalledWith('save');
});
it('handles Ctrl+Z', () => {
const mockShortcut = jest.fn();
const wrapper = shallow(<ShortcutHandler onShortcut={mockShortcut} />);
wrapper.find('div').simulate('keydown', {
ctrlKey: true,
key: 'z',
preventDefault: jest.fn()
});
expect(mockShortcut).toHaveBeenCalledWith('undo');
});
});
特殊按键 #
javascript
function ArrowNavigation({ onNavigate }) {
const handleKeyDown = (e) => {
switch (e.key) {
case 'ArrowUp':
onNavigate('up');
break;
case 'ArrowDown':
onNavigate('down');
break;
case 'ArrowLeft':
onNavigate('left');
break;
case 'ArrowRight':
onNavigate('right');
break;
default:
break;
}
};
return <div onKeyDown={handleKeyDown} />;
}
describe('ArrowNavigation', () => {
it('handles arrow keys', () => {
const mockNavigate = jest.fn();
const wrapper = shallow(<ArrowNavigation onNavigate={mockNavigate} />);
wrapper.find('div').simulate('keydown', { key: 'ArrowUp' });
expect(mockNavigate).toHaveBeenCalledWith('up');
wrapper.find('div').simulate('keydown', { key: 'ArrowDown' });
expect(mockNavigate).toHaveBeenCalledWith('down');
wrapper.find('div').simulate('keydown', { key: 'ArrowLeft' });
expect(mockNavigate).toHaveBeenCalledWith('left');
wrapper.find('div').simulate('keydown', { key: 'ArrowRight' });
expect(mockNavigate).toHaveBeenCalledWith('right');
});
});
焦点事件 #
focus 和 blur #
javascript
function InputWithValidation({ validate }) {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const [focused, setFocused] = useState(false);
const handleFocus = () => {
setFocused(true);
setError('');
};
const handleBlur = () => {
setFocused(false);
const validationError = validate(value);
if (validationError) {
setError(validationError);
}
};
return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
/>
{error && <span className="error">{error}</span>}
{focused && <span className="hint">Enter a valid value</span>}
</div>
);
}
describe('InputWithValidation', () => {
it('shows hint on focus', () => {
const wrapper = shallow(
<InputWithValidation validate={jest.fn()} />
);
wrapper.find('input').simulate('focus');
expect(wrapper.find('.hint').exists()).toBe(true);
});
it('validates on blur', () => {
const mockValidate = jest.fn().mockReturnValue('Invalid input');
const wrapper = shallow(
<InputWithValidation validate={mockValidate} />
);
wrapper.find('input').simulate('change', {
target: { value: 'test' }
});
wrapper.find('input').simulate('blur');
expect(mockValidate).toHaveBeenCalledWith('test');
expect(wrapper.find('.error').text()).toBe('Invalid input');
});
it('clears error on focus', () => {
const mockValidate = jest.fn().mockReturnValue('Error');
const wrapper = shallow(
<InputWithValidation validate={mockValidate} />
);
wrapper.find('input').simulate('change', {
target: { value: 'test' }
});
wrapper.find('input').simulate('blur');
expect(wrapper.find('.error').exists()).toBe(true);
wrapper.find('input').simulate('focus');
expect(wrapper.find('.error').exists()).toBe(false);
});
});
鼠标事件 #
悬停效果 #
javascript
function TooltipButton({ tooltip, children }) {
const [showTooltip, setShowTooltip] = useState(false);
return (
<div className="tooltip-container">
<button
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
{children}
</button>
{showTooltip && <span className="tooltip">{tooltip}</span>}
</div>
);
}
describe('TooltipButton', () => {
it('shows tooltip on mouse enter', () => {
const wrapper = shallow(
<TooltipButton tooltip="Click me!">Button</TooltipButton>
);
expect(wrapper.find('.tooltip').exists()).toBe(false);
wrapper.find('button').simulate('mouseEnter');
expect(wrapper.find('.tooltip').exists()).toBe(true);
expect(wrapper.find('.tooltip').text()).toBe('Click me!');
});
it('hides tooltip on mouse leave', () => {
const wrapper = shallow(
<TooltipButton tooltip="Click me!">Button</TooltipButton>
);
wrapper.find('button').simulate('mouseEnter');
expect(wrapper.find('.tooltip').exists()).toBe(true);
wrapper.find('button').simulate('mouseLeave');
expect(wrapper.find('.tooltip').exists()).toBe(false);
});
});
拖拽模拟 #
javascript
function Draggable({ onDragStart, onDragEnd }) {
return (
<div
draggable
onDragStart={(e) => onDragStart(e.target.id)}
onDragEnd={(e) => onDragEnd(e.target.id)}
>
Drag me
</div>
);
}
describe('Draggable', () => {
it('handles drag start', () => {
const mockDragStart = jest.fn();
const wrapper = shallow(
<Draggable onDragStart={mockDragStart} onDragEnd={jest.fn()} />
);
wrapper.find('div').simulate('dragStart', { target: { id: 'item-1' } });
expect(mockDragStart).toHaveBeenCalledWith('item-1');
});
it('handles drag end', () => {
const mockDragEnd = jest.fn();
const wrapper = shallow(
<Draggable onDragStart={jest.fn()} onDragEnd={mockDragEnd} />
);
wrapper.find('div').simulate('dragEnd', { target: { id: 'item-1' } });
expect(mockDragEnd).toHaveBeenCalledWith('item-1');
});
});
高级交互 #
模拟事件对象 #
javascript
function ComplexForm() {
const handleSubmit = (e) => {
e.preventDefault();
e.stopPropagation();
// 处理提交
};
return <form onSubmit={handleSubmit}>...</form>;
}
describe('ComplexForm', () => {
it('handles complex event', () => {
const wrapper = shallow(<ComplexForm />);
const mockEvent = {
preventDefault: jest.fn(),
stopPropagation: jest.fn()
};
wrapper.find('form').simulate('submit', mockEvent);
expect(mockEvent.preventDefault).toHaveBeenCalled();
expect(mockEvent.stopPropagation).toHaveBeenCalled();
});
});
直接调用 props 方法 #
javascript
function Button({ onClick }) {
return <button onClick={onClick}>Click</button>;
}
describe('Button', () => {
it('can call onClick directly', () => {
const mockClick = jest.fn();
const wrapper = shallow(<Button onClick={mockClick} />);
// 方式 1: simulate
wrapper.find('button').simulate('click');
// 方式 2: 直接调用 prop
wrapper.find('button').props().onClick();
expect(mockClick).toHaveBeenCalledTimes(2);
});
});
调用实例方法 #
javascript
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment() {
this.setState({ count: this.state.count + 1 });
}
decrement() {
this.setState({ count: this.state.count - 1 });
}
render() {
return <div>{this.state.count}</div>;
}
}
describe('Counter instance methods', () => {
it('can call instance methods directly', () => {
const wrapper = shallow(<Counter />);
wrapper.instance().increment();
expect(wrapper.state('count')).toBe(1);
wrapper.instance().decrement();
expect(wrapper.state('count')).toBe(0);
});
});
simulate 的局限性 #
simulate 不是真实事件 #
javascript
// simulate 只是调用事件处理函数
// 它不会触发真正的事件传播
function Parent() {
return (
<div onClick={() => console.log('parent clicked')}>
<button onClick={(e) => e.stopPropagation()}>
Click
</button>
</div>
);
}
describe('Event propagation', () => {
it('does not test real event propagation', () => {
const wrapper = shallow(<Parent />);
// 在 shallow 渲染中,事件传播不会发生
// simulate 只是调用按钮的 onClick
wrapper.find('button').simulate('click');
// 如果需要测试事件传播,应该使用 mount
});
});
何时使用 mount #
javascript
// 需要真实 DOM 交互时使用 mount
describe('Real DOM interactions', () => {
it('tests event propagation with mount', () => {
const parentClick = jest.fn();
const childClick = jest.fn();
const wrapper = mount(
<div onClick={parentClick}>
<button onClick={childClick}>Click</button>
</div>
);
wrapper.find('button').simulate('click');
// 在 mount 中,事件会传播
expect(childClick).toHaveBeenCalled();
wrapper.unmount();
});
});
最佳实践 #
1. 模拟最小必要的事件数据 #
javascript
// ✅ 好的做法 - 只提供必要的数据
wrapper.find('input').simulate('change', {
target: { value: 'test' }
});
// ❌ 避免 - 过度模拟
wrapper.find('input').simulate('change', {
target: {
value: 'test',
name: 'input',
id: 'input-1',
type: 'text',
// ... 很多不必要的属性
},
bubbles: true,
cancelable: true,
// ... 很多不必要的属性
});
2. 使用辅助函数 #
javascript
// 创建事件模拟辅助函数
const createChangeEvent = (value, name = '') => ({
target: { value, name }
});
const createClickEvent = () => ({
preventDefault: jest.fn(),
stopPropagation: jest.fn()
});
// 使用
wrapper.find('input').simulate('change', createChangeEvent('test'));
wrapper.find('form').simulate('submit', createClickEvent());
3. 测试用户行为 #
javascript
// ✅ 好的做法 - 测试用户可见的行为
it('shows error when input is empty', () => {
const wrapper = shallow(<Form />);
wrapper.find('button').simulate('click');
expect(wrapper.find('.error').text()).toBe('Field is required');
});
// ❌ 避免 - 测试实现细节
it('sets error state', () => {
const wrapper = shallow(<Form />);
wrapper.find('button').simulate('click');
expect(wrapper.state('error')).toBe(true); // 实现细节
});
下一步 #
现在你已经掌握了交互模拟的使用方法,接下来学习 状态与属性测试 了解如何测试组件的状态和属性!
最后更新:2026-03-28