Enzyme 选择器 #

选择器概述 #

Enzyme 提供了强大的选择器系统,可以灵活地查找组件中的元素。选择器语法类似于 jQuery 和 CSS,让开发者可以快速上手。

选择器类型 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Enzyme 选择器类型                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. CSS 选择器                                              │
│     ├── 标签选择器: 'div', 'span', 'button'                 │
│     ├── 类选择器: '.class-name'                             │
│     ├── ID 选择器: '#element-id'                            │
│     └── 属性选择器: '[type="submit"]'                       │
│                                                             │
│  2. 组件选择器                                              │
│     ├── 组件引用: MyComponent                               │
│     └── 组件名称: 'MyComponent'                             │
│                                                             │
│  3. 复合选择器                                              │
│     ├── 后代选择器: 'div .class'                            │
│     ├── 子选择器: 'div > .class'                            │
│     └── 组合选择器: 'div.class#id'                          │
│                                                             │
│  4. 伪类选择器                                              │
│     └── ':first-child', ':last-child', ':not()'            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

基本选择器 #

标签选择器 #

通过 HTML 标签名查找元素:

javascript
const wrapper = shallow(<MyComponent />);

// 查找所有 div 元素
wrapper.find('div');

// 查找所有 button 元素
wrapper.find('button');

// 查找所有 input 元素
wrapper.find('input');

// 查找所有 span 元素
wrapper.find('span');

类选择器 #

通过 CSS 类名查找元素:

javascript
const wrapper = shallow(<MyComponent />);

// 查找具有 'btn' 类的元素
wrapper.find('.btn');

// 查找具有 'container' 类的元素
wrapper.find('.container');

// 查找具有多个类的元素
wrapper.find('.btn.primary.active');

ID 选择器 #

通过元素 ID 查找:

javascript
const wrapper = shallow(<MyComponent />);

// 查找 ID 为 'header' 的元素
wrapper.find('#header');

// 查找 ID 为 'submit-button' 的元素
wrapper.find('#submit-button');

属性选择器 #

通过元素属性查找:

javascript
const wrapper = shallow(<MyComponent />);

// 查找具有 type 属性的元素
wrapper.find('[type]');

// 查找 type="submit" 的元素
wrapper.find('[type="submit"]');

// 查找 data-testid 属性
wrapper.find('[data-testid="submit-button"]');

// 查找 disabled 属性
wrapper.find('[disabled]');

// 查找 name 属性
wrapper.find('[name="email"]');

组件选择器 #

通过组件引用查找 #

javascript
import ChildComponent from './ChildComponent';

function ParentComponent() {
  return (
    <div>
      <ChildComponent title="Hello" />
      <ChildComponent title="World" />
    </div>
  );
}

describe('ParentComponent', () => {
  it('finds child components', () => {
    const wrapper = shallow(<ParentComponent />);
    
    // 通过组件引用查找
    const children = wrapper.find(ChildComponent);
    expect(children.length).toBe(2);
    
    // 访问第一个子组件的 props
    expect(children.at(0).prop('title')).toBe('Hello');
  });
});

通过组件名称查找 #

javascript
describe('ParentComponent', () => {
  it('finds by component name', () => {
    const wrapper = shallow(<ParentComponent />);
    
    // 通过组件名称字符串查找
    const children = wrapper.find('ChildComponent');
    expect(children.length).toBe(2);
  });
});

组件显示名称 #

javascript
// 组件显示名称
const MyButton = () => <button>Click</button>;
MyButton.displayName = 'MyButton';

function App() {
  return (
    <div>
      <MyButton />
    </div>
  );
}

describe('App', () => {
  it('finds by display name', () => {
    const wrapper = shallow(<App />);
    
    // 通过显示名称查找
    expect(wrapper.find('MyButton').length).toBe(1);
    expect(wrapper.find(MyButton).length).toBe(1);
  });
});

复合选择器 #

后代选择器 #

查找后代元素(任意层级):

javascript
const wrapper = shallow(<MyComponent />);

// 查找 div 内的所有 button
wrapper.find('div button');

// 查找 .container 内的所有 .item
wrapper.find('.container .item');

// 查找 form 内的所有 input
wrapper.find('form input');

子选择器 #

查找直接子元素:

javascript
const wrapper = shallow(<MyComponent />);

// 查找 div 的直接子元素 button
wrapper.find('div > button');

// 查找 ul 的直接子元素 li
wrapper.find('ul > li');

// 查找 .container 的直接子元素 .item
wrapper.find('.container > .item');

组合选择器 #

同时匹配多个条件:

javascript
const wrapper = shallow(<MyComponent />);

// 同时匹配标签和类名
wrapper.find('button.primary');

// 同时匹配标签、类名和 ID
wrapper.find('button.primary#submit');

// 同时匹配标签和属性
wrapper.find('input[type="email"]');

// 复杂组合
wrapper.find('button.primary[type="submit"][disabled]');

伪类选择器 #

:first-child 和 :last-child #

javascript
const wrapper = shallow(<MyComponent />);

// 查找第一个子元素
wrapper.find('li:first-child');

// 查找最后一个子元素
wrapper.find('li:last-child');

:not() #

javascript
const wrapper = shallow(<MyComponent />);

// 查找不具有 disabled 类的 button
wrapper.find('button:not(.disabled)');

// 查找不具有 active 类的 item
wrapper.find('.item:not(.active)');

:nth-child() #

javascript
const wrapper = shallow(<MyComponent />);

// 查找第 n 个子元素
wrapper.find('li:nth-child(2)');

// 查找奇数位置的子元素
wrapper.find('li:nth-child(odd)');

// 查找偶数位置的子元素
wrapper.find('li:nth-child(even)');

选择器方法 #

find() #

查找匹配选择器的所有元素:

javascript
const wrapper = shallow(<MyComponent />);

// 返回所有匹配的元素
const buttons = wrapper.find('button');
console.log(buttons.length); // 匹配数量

filter() #

过滤当前集合中的元素:

javascript
const wrapper = shallow(<MyComponent />);

// 先找所有 li,再过滤出 active 的
const activeItems = wrapper.find('li').filter('.active');

// 使用函数过滤
const longItems = wrapper.find('li').filterWhere(node => {
  return node.text().length > 10;
});

filterWhere() #

使用自定义函数过滤:

javascript
const wrapper = shallow(<MyComponent />);

// 过滤文本长度大于 5 的元素
const filtered = wrapper.find('li').filterWhere(node => {
  return node.text().length > 5;
});

// 过滤包含特定属性的元素
const withDataId = wrapper.find('*').filterWhere(node => {
  return node.prop('data-testid') !== undefined;
});

not() #

排除匹配的元素:

javascript
const wrapper = shallow(<MyComponent />);

// 排除 disabled 类的元素
const enabledButtons = wrapper.find('button').not('.disabled');

// 排除特定属性
const visibleItems = wrapper.find('.item').not('[hidden]');

遍历方法 #

at() #

获取指定索引的元素:

javascript
const wrapper = shallow(<MyComponent />);
const items = wrapper.find('li');

// 获取第一个元素(索引 0)
const firstItem = items.at(0);

// 获取第三个元素
const thirdItem = items.at(2);

first() 和 last() #

javascript
const wrapper = shallow(<MyComponent />);
const items = wrapper.find('li');

// 获取第一个元素
const first = items.first();

// 获取最后一个元素
const last = items.last();

parent() #

获取父元素:

javascript
const wrapper = shallow(<MyComponent />);
const button = wrapper.find('button');

// 获取 button 的父元素
const parent = button.parent();

parents() #

获取所有祖先元素:

javascript
const wrapper = shallow(<MyComponent />);
const button = wrapper.find('button');

// 获取所有祖先元素
const parents = button.parents();

// 获取特定类型的祖先
const formParent = button.parents('form');

closest() #

获取最近的匹配祖先:

javascript
const wrapper = shallow(<MyComponent />);
const button = wrapper.find('button');

// 获取最近的 form 祖先
const form = button.closest('form');

// 获取最近的 .container 祖先
const container = button.closest('.container');

children() #

获取所有子元素:

javascript
const wrapper = shallow(<MyComponent />);
const container = wrapper.find('.container');

// 获取所有子元素
const children = container.children();

// 获取特定选择器的子元素
const activeChildren = container.children('.active');

childAt() #

获取指定索引的子元素:

javascript
const wrapper = shallow(<MyComponent />);
const container = wrapper.find('.container');

// 获取第一个子元素
const firstChild = container.childAt(0);

// 获取第三个子元素
const thirdChild = container.childAt(2);

siblings() #

获取兄弟元素:

javascript
const wrapper = shallow(<MyComponent />);
const activeItem = wrapper.find('.item.active');

// 获取所有兄弟元素
const siblings = activeItem.siblings();

// 获取特定选择器的兄弟元素
const inactiveSiblings = activeItem.siblings('.item:not(.active)');

实用示例 #

测试表单元素 #

javascript
function LoginForm() {
  return (
    <form className="login-form">
      <input 
        type="email" 
        name="email" 
        className="input-email"
        data-testid="email-input"
      />
      <input 
        type="password" 
        name="password" 
        className="input-password"
        data-testid="password-input"
      />
      <button 
        type="submit" 
        className="btn-submit"
        data-testid="submit-button"
      >
        Login
      </button>
    </form>
  );
}

describe('LoginForm selectors', () => {
  let wrapper;
  
  beforeEach(() => {
    wrapper = shallow(<LoginForm />);
  });
  
  it('finds inputs by type', () => {
    expect(wrapper.find('input[type="email"]').length).toBe(1);
    expect(wrapper.find('input[type="password"]').length).toBe(1);
  });
  
  it('finds inputs by name', () => {
    expect(wrapper.find('input[name="email"]').length).toBe(1);
    expect(wrapper.find('input[name="password"]').length).toBe(1);
  });
  
  it('finds inputs by className', () => {
    expect(wrapper.find('.input-email').length).toBe(1);
    expect(wrapper.find('.input-password').length).toBe(1);
  });
  
  it('finds inputs by data-testid', () => {
    expect(wrapper.find('[data-testid="email-input"]').length).toBe(1);
    expect(wrapper.find('[data-testid="password-input"]').length).toBe(1);
  });
  
  it('finds submit button', () => {
    expect(wrapper.find('button[type="submit"]').length).toBe(1);
    expect(wrapper.find('.btn-submit').length).toBe(1);
    expect(wrapper.find('[data-testid="submit-button"]').length).toBe(1);
  });
});

测试列表组件 #

javascript
function TodoList({ items }) {
  return (
    <ul className="todo-list">
      {items.map((item, index) => (
        <li 
          key={item.id} 
          className={`todo-item ${item.completed ? 'completed' : ''}`}
          data-id={item.id}
        >
          <span className="todo-text">{item.text}</span>
          <button 
            className="delete-btn"
            data-action="delete"
          >
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

describe('TodoList selectors', () => {
  const items = [
    { id: 1, text: 'Task 1', completed: false },
    { id: 2, text: 'Task 2', completed: true },
    { id: 3, text: 'Task 3', completed: false }
  ];
  
  let wrapper;
  
  beforeEach(() => {
    wrapper = shallow(<TodoList items={items} />);
  });
  
  it('finds all list items', () => {
    expect(wrapper.find('li').length).toBe(3);
    expect(wrapper.find('.todo-item').length).toBe(3);
  });
  
  it('finds completed items', () => {
    expect(wrapper.find('.todo-item.completed').length).toBe(1);
  });
  
  it('finds incomplete items', () => {
    expect(wrapper.find('.todo-item:not(.completed)').length).toBe(2);
  });
  
  it('finds item by data-id', () => {
    expect(wrapper.find('[data-id="1"]').length).toBe(1);
    expect(wrapper.find('[data-id="2"]').length).toBe(1);
  });
  
  it('finds delete buttons', () => {
    expect(wrapper.find('.delete-btn').length).toBe(3);
    expect(wrapper.find('[data-action="delete"]').length).toBe(3);
  });
  
  it('finds text spans', () => {
    const texts = wrapper.find('.todo-text').map(node => node.text());
    expect(texts).toEqual(['Task 1', 'Task 2', 'Task 3']);
  });
});

测试嵌套组件 #

javascript
function Card({ title, children }) {
  return (
    <div className="card">
      <header className="card-header">
        <h2 className="card-title">{title}</h2>
      </header>
      <section className="card-body">
        {children}
      </section>
    </div>
  );
}

function App() {
  return (
    <div className="app">
      <Card title="Card 1">
        <p>Content 1</p>
      </Card>
      <Card title="Card 2">
        <p>Content 2</p>
      </Card>
    </div>
  );
}

describe('Nested components', () => {
  let wrapper;
  
  beforeEach(() => {
    wrapper = shallow(<App />);
  });
  
  it('finds all cards', () => {
    expect(wrapper.find(Card).length).toBe(2);
    expect(wrapper.find('Card').length).toBe(2);
  });
  
  it('finds first card', () => {
    const firstCard = wrapper.find(Card).first();
    expect(firstCard.prop('title')).toBe('Card 1');
  });
  
  it('finds card by index', () => {
    const secondCard = wrapper.find(Card).at(1);
    expect(secondCard.prop('title')).toBe('Card 2');
  });
});

describe('Card internal structure', () => {
  let wrapper;
  
  beforeEach(() => {
    wrapper = shallow(<Card title="Test Card">Content</Card>);
  });
  
  it('finds header and body', () => {
    expect(wrapper.find('.card-header').length).toBe(1);
    expect(wrapper.find('.card-body').length).toBe(1);
  });
  
  it('finds title', () => {
    expect(wrapper.find('.card-title').text()).toBe('Test Card');
  });
  
  it('finds children in body', () => {
    const body = wrapper.find('.card-body');
    expect(body.text()).toBe('Content');
  });
});

选择器最佳实践 #

1. 使用 data-testid #

javascript
// ✅ 推荐:使用 data-testid
wrapper.find('[data-testid="submit-button"]');

// ❌ 避免:依赖 CSS 类名(可能变化)
wrapper.find('.btn-primary-submit');

2. 选择器优先级 #

javascript
// 优先级从高到低:

// 1. data-testid(最稳定)
wrapper.find('[data-testid="submit-button"]');

// 2. 语义化的属性选择器
wrapper.find('button[type="submit"]');
wrapper.find('input[name="email"]');

// 3. 组件选择器
wrapper.find(MyComponent);

// 4. 语义化的类名
wrapper.find('.submit-button');

// 5. 标签选择器(最后选择)
wrapper.find('button');

3. 避免脆弱的选择器 #

javascript
// ❌ 脆弱的选择器
wrapper.find('div > div > div > button'); // 依赖嵌套结构
wrapper.find('.css-1a2b3c'); // 自动生成的类名
wrapper.find('#root > div:nth-child(3)'); // 依赖位置

// ✅ 稳健的选择器
wrapper.find('[data-testid="submit-button"]');
wrapper.find('button[type="submit"]');
wrapper.find('.submit-button');

4. 组合使用选择器 #

javascript
// 组合使用提高精确度
wrapper.find('button.primary[type="submit"][data-testid="submit"]');

// 但不要过度组合
// ❌ 太复杂
wrapper.find('div.container > form.login-form .fields .field input[type="text"][name="email"]');

// ✅ 适度组合
wrapper.find('input[name="email"]');
wrapper.find('[data-testid="email-input"]');

下一步 #

现在你已经掌握了选择器的使用方法,接下来学习 交互模拟 了解如何模拟用户交互!

最后更新:2026-03-28