Testing Library 查询方法 #

查询方法分类 #

Testing Library 的查询方法按优先级分为三类:

text
┌─────────────────────────────────────────────────────────────┐
│                     查询优先级                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  第一优先级 - 推荐(无障碍性)                               │
│  ├── getByRole          角色查询                            │
│  ├── getByLabelText     标签关联查询                        │
│  ├── getByPlaceholderText 占位符查询                        │
│  ├── getByText          文本内容查询                        │
│  └── getByDisplayValue  显示值查询                          │
│                                                             │
│  第二优先级 - 语义化                                        │
│  ├── getByAltText       替代文本查询                        │
│  └── getByTitle         标题属性查询                        │
│                                                             │
│  第三优先级 - 最后手段                                      │
│  └── getByTestId        测试 ID 查询                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

getByRole - 角色查询 #

什么是 ARIA Role #

ARIA Role 定义了元素在无障碍树中的角色:

jsx
function ButtonExamples() {
  return (
    <div>
      <button>Button</button>
      <a href="#">Link</a>
      <input type="text" />
      <input type="checkbox" />
      <h1>Heading</h1>
      <ul><li>Item</li></ul>
    </div>
  );
}

test('elements have implicit roles', () => {
  render(<ButtonExamples />);
  
  screen.getByRole('button');
  screen.getByRole('link');
  screen.getByRole('textbox');
  screen.getByRole('checkbox');
  screen.getByRole('heading');
  screen.getByRole('list');
});

常用隐式 Role #

元素 隐式 Role
<button> button
<a href="..."> link
<input type="text"> textbox
<input type="checkbox"> checkbox
<input type="radio"> radio
<input type="number"> spinbutton
<input type="range"> slider
<select> combobox / listbox
<textarea> textbox
<h1> - <h6> heading
<ul>, <ol> list
<li> listitem
<table> table
<tr> row
<td> cell
<th> columnheader / rowheader
<form> form
<img alt="..."> img
<nav> navigation
<main> main
<header> banner
<footer> contentinfo
<aside> complementary
<dialog> dialog
<progress> progressbar

name 选项 #

通过可访问名称过滤:

jsx
function Form() {
  return (
    <form>
      <button type="submit">Submit</button>
      <button type="button">Cancel</button>
    </form>
  );
}

test('name option filters by accessible name', () => {
  render(<Form />);
  
  screen.getByRole('button', { name: 'Submit' });
  screen.getByRole('button', { name: 'Cancel' });
  
  // 正则表达式
  screen.getByRole('button', { name: /submit/i });
});

可访问名称来源 #

jsx
function AccessibleNames() {
  return (
    <div>
      {/* 按钮的文本内容 */}
      <button>Save Changes</button>
      
      {/* aria-label 属性 */}
      <button aria-label="Close dialog">×</button>
      
      {/* aria-labelledby 属性 */}
      <span id="label">Delete</span>
      <button aria-labelledby="label">🗑️</button>
      
      {/* 图像的 alt 属性 */}
      <button><img alt="Add item" src="plus.svg" /></button>
      
      {/* 表单标签 */}
      <label htmlFor="email">Email</label>
      <input id="email" type="text" />
    </div>
  );
}

test('accessible name sources', () => {
  render(<AccessibleNames />);
  
  screen.getByRole('button', { name: 'Save Changes' });
  screen.getByRole('button', { name: 'Close dialog' });
  screen.getByRole('button', { name: 'Delete' });
  screen.getByRole('button', { name: 'Add item' });
  screen.getByRole('textbox', { name: 'Email' });
});

查询选项 #

jsx
function ComplexForm() {
  return (
    <div>
      <button disabled>Disabled Button</button>
      <button>Enabled Button</button>
      <input type="text" required />
      <input type="checkbox" checked />
      <select>
        <option value="">Select...</option>
        <option value="1">Option 1</option>
        <option value="2">Option 2</option>
      </select>
    </div>
  );
}

test('role query options', () => {
  render(<ComplexForm />);
  
  // hidden: 包含隐藏元素
  screen.getByRole('button', { hidden: true });
  
  // 查询特定状态
  screen.getByRole('button', { name: 'Disabled Button' });
  expect(screen.getByRole('button', { name: 'Disabled Button' })).toBeDisabled();
  
  // 查询表单元素
  screen.getByRole('textbox');
  screen.getByRole('checkbox');
  screen.getByRole('combobox');
});

自定义 Role #

jsx
function CustomRole() {
  return (
    <div>
      <div role="alert">Error message</div>
      <div role="status">Loading...</div>
      <div role="progressbar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100">50%</div>
      <div role="tablist">
        <button role="tab" aria-selected="true">Tab 1</button>
        <button role="tab" aria-selected="false">Tab 2</button>
      </div>
    </div>
  );
}

test('custom roles', () => {
  render(<CustomRole />);
  
  screen.getByRole('alert');
  screen.getByRole('status');
  screen.getByRole('progressbar');
  screen.getByRole('tablist');
  screen.getByRole('tab', { name: 'Tab 1' });
  screen.getByRole('tab', { name: 'Tab 2', selected: false });
});

getByLabelText - 标签查询 #

基本用法 #

jsx
function Form() {
  return (
    <form>
      <label htmlFor="username">Username</label>
      <input id="username" type="text" />
      
      <label htmlFor="password">Password</label>
      <input id="password" type="password" />
      
      <label>
        Email
        <input type="email" />
      </label>
      
      <label>
        <input type="checkbox" />
        Remember me
      </label>
    </form>
  );
}

test('getByLabelText finds inputs by label', () => {
  render(<Form />);
  
  screen.getByLabelText('Username');
  screen.getByLabelText('Password');
  screen.getByLabelText('Email');
  screen.getByLabelText('Remember me');
});

标签关联方式 #

jsx
function LabelExamples() {
  return (
    <form>
      {/* for/id 关联 */}
      <label htmlFor="name">Name</label>
      <input id="name" />
      
      {/* 嵌套关联 */}
      <label>
        Age
        <input type="number" />
      </label>
      
      {/* aria-labelledby */}
      <span id="phone-label">Phone</span>
      <input aria-labelledby="phone-label" />
      
      {/* aria-label */}
      <input aria-label="Search" type="search" />
    </form>
  );
}

test('label association methods', () => {
  render(<LabelExamples />);
  
  screen.getByLabelText('Name');
  screen.getByLabelText('Age');
  screen.getByLabelText('Phone');
  screen.getByLabelText('Search');
});

选择器选项 #

jsx
function MultipleInputs() {
  return (
    <form>
      <label htmlFor="email">Email</label>
      <input id="email" type="email" />
      
      <label htmlFor="email-confirm">Confirm Email</label>
      <input id="email-confirm" type="email" />
    </form>
  );
}

test('selector options', () => {
  render(<MultipleInputs />);
  
  // 精确匹配
  screen.getByLabelText('Email', { selector: 'input' });
  
  // 正则匹配
  screen.getByLabelText(/confirm email/i);
});

getByPlaceholderText - 占位符查询 #

基本用法 #

jsx
function SearchForm() {
  return (
    <form>
      <input placeholder="Search products..." />
      <input placeholder="Enter your email" type="email" />
      <textarea placeholder="Write your message..." />
    </form>
  );
}

test('getByPlaceholderText', () => {
  render(<SearchForm />);
  
  screen.getByPlaceholderText('Search products...');
  screen.getByPlaceholderText('Enter your email');
  screen.getByPlaceholderText('Write your message...');
  
  // 正则匹配
  screen.getByPlaceholderText(/search/i);
});

适用场景 #

jsx
function LoginForm() {
  return (
    <form>
      {/* 无标签时使用占位符 */}
      <input placeholder="Username or email" />
      <input placeholder="Password" type="password" />
    </form>
  );
}

test('placeholder for label-less inputs', () => {
  render(<LoginForm />);
  
  // 当没有 label 时,placeholder 是好的选择
  screen.getByPlaceholderText('Username or email');
  screen.getByPlaceholderText('Password');
});

getByText - 文本查询 #

基本用法 #

jsx
function Article() {
  return (
    <article>
      <h1>Introduction to Testing</h1>
      <p>This article covers the basics of testing.</p>
      <p>Testing is important for software quality.</p>
      <button>Read More</button>
    </article>
  );
}

test('getByText finds elements by text content', () => {
  render(<Article />);
  
  screen.getByText('Introduction to Testing');
  screen.getByText('This article covers the basics of testing.');
  screen.getByText('Read More');
  
  // 正则匹配
  screen.getByText(/introduction/i);
  screen.getByText(/testing.*important/);
});

匹配选项 #

jsx
function ProductCard({ name, price }) {
  return (
    <div>
      <h2>{name}</h2>
      <span>Price: ${price}</span>
      <p>In stock</p>
    </div>
  );
}

test('text matching options', () => {
  render(<ProductCard name="Widget" price={29.99} />);
  
  // exact: true (默认)
  screen.getByText('Price: $29.99');
  
  // exact: false - 部分匹配
  screen.getByText('Widget', { exact: false });
  screen.getByText('Price', { exact: false });
  
  // 正则表达式
  screen.getByText(/\$29\.99/);
  screen.getByText(/Price: \$\d+\.\d{2}/);
});

文本选择器 #

jsx
function MixedContent() {
  return (
    <div>
      <p>Hello <strong>World</strong></p>
      <span className="highlight">Important</span>
      <div>Text in div</div>
    </div>
  );
}

test('text selector options', () => {
  render(<MixedContent />);
  
  // 默认匹配所有元素
  screen.getByText('World');
  
  // 指定选择器
  screen.getByText('Important', { selector: '.highlight' });
  screen.getByText('Text in div', { selector: 'div' });
  
  // 忽略子元素
  screen.getByText('Hello', { exact: false, selector: 'p' });
});

处理动态文本 #

jsx
function Timer({ seconds }) {
  return <div>Time remaining: {seconds}s</div>;
}

test('dynamic text with regex', () => {
  render(<Timer seconds={30} />);
  
  // 使用正则匹配动态内容
  screen.getByText(/Time remaining: \d+s/);
  
  // 使用函数匹配器
  screen.getByText((content, element) => {
    return content.startsWith('Time remaining:');
  });
});

getByDisplayValue - 显示值查询 #

基本用法 #

jsx
function ControlledForm() {
  const [name, setName] = useState('John');
  const [email, setEmail] = useState('john@example.com');
  
  return (
    <form>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <select value="option1">
        <option value="option1">Option 1</option>
        <option value="option2">Option 2</option>
      </select>
    </form>
  );
}

test('getByDisplayValue finds inputs by value', () => {
  render(<ControlledForm />);
  
  screen.getByDisplayValue('John');
  screen.getByDisplayValue('john@example.com');
  screen.getByDisplayValue('Option 1');
});

适用场景 #

jsx
function SearchFilter({ filters, onFilterChange }) {
  return (
    <div>
      <select 
        value={filters.category} 
        onChange={(e) => onFilterChange({ category: e.target.value })}
      >
        <option value="">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="books">Books</option>
      </select>
      
      <select
        value={filters.sort}
        onChange={(e) => onFilterChange({ sort: e.target.value })}
      >
        <option value="newest">Newest</option>
        <option value="price-asc">Price: Low to High</option>
      </select>
    </div>
  );
}

test('verify filter selections', () => {
  const filters = { category: 'electronics', sort: 'newest' };
  render(<SearchFilter filters={filters} onFilterChange={() => {}} />);
  
  screen.getByDisplayValue('Electronics');
  screen.getByDisplayValue('Newest');
});

getByAltText - 替代文本查询 #

基本用法 #

jsx
function Gallery() {
  return (
    <div>
      <img src="/photo1.jpg" alt="Sunset over the ocean" />
      <img src="/photo2.jpg" alt="Mountain landscape" />
      <input type="image" alt="Submit form" src="submit.png" />
    </div>
  );
}

test('getByAltText finds images by alt text', () => {
  render(<Gallery />);
  
  screen.getByAltText('Sunset over the ocean');
  screen.getByAltText('Mountain landscape');
  screen.getByAltText('Submit form');
});

适用元素 #

jsx
function AltTextExamples() {
  return (
    <div>
      {/* img 元素 */}
      <img alt="Product photo" src="product.jpg" />
      
      {/* input type="image" */}
      <input type="image" alt="Submit" src="submit.png" />
      
      {/* area 元素(图像地图) */}
      <map name="workmap">
        <area alt="Computer" coords="34,44,270,350" />
      </map>
    </div>
  );
}

test('elements supporting alt text', () => {
  render(<AltTextExamples />);
  
  screen.getByAltText('Product photo');
  screen.getByAltText('Submit');
  screen.getByAltText('Computer');
});

getByTitle - 标题属性查询 #

基本用法 #

jsx
function IconButtons() {
  return (
    <div>
      <button title="Close window">×</button>
      <button title="Save changes">💾</button>
      <span title="More information">ℹ️</span>
      <svg title="Warning icon">...</svg>
    </div>
  );
}

test('getByTitle finds elements by title attribute', () => {
  render(<IconButtons />);
  
  screen.getByTitle('Close window');
  screen.getByTitle('Save changes');
  screen.getByTitle('More information');
  screen.getByTitle('Warning icon');
});

SVG title 元素 #

jsx
function Icon() {
  return (
    <svg>
      <title>Shopping cart</title>
      <path d="..." />
    </svg>
  );
}

test('svg title element', () => {
  render(<Icon />);
  
  screen.getByTitle('Shopping cart');
});

getByTestId - 测试 ID 查询 #

基本用法 #

jsx
function DynamicList({ items }) {
  return (
    <ul data-testid="item-list">
      {items.map((item) => (
        <li key={item.id} data-testid={`item-${item.id}`}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

test('getByTestId finds elements by data-testid', () => {
  const items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
  ];
  
  render(<DynamicList items={items} />);
  
  screen.getByTestId('item-list');
  screen.getByTestId('item-1');
  screen.getByTestId('item-2');
});

何时使用 #

jsx
function Chart({ data }) {
  return (
    <div data-testid="chart-container">
      {/* 复杂的可视化,没有语义化标签 */}
      <svg data-testid="chart-svg">
        {/* ... */}
      </svg>
    </div>
  );
}

test('use testId for non-semantic elements', () => {
  render(<Chart data={[]} />);
  
  // 当没有语义化查询可用时
  screen.getByTestId('chart-container');
  screen.getByTestId('chart-svg');
});

自定义 testId 属性 #

javascript
// jest.setup.js
import { configure } from '@testing-library/react';

configure({
  testIdAttribute: 'data-cy', // 使用 Cypress 风格
});
jsx
function Component() {
  return <div data-cy="my-element">Content</div>;
}

test('custom testId attribute', () => {
  render(<Component />);
  
  screen.getByTestId('my-element');
});

组合查询 #

使用 within 限定范围 #

jsx
import { render, screen, within } from '@testing-library/react';

function Dashboard() {
  return (
    <div>
      <header>
        <nav>
          <a href="/">Home</a>
          <a href="/about">About</a>
        </nav>
      </header>
      
      <main>
        <nav>
          <a href="/dashboard">Dashboard</a>
          <a href="/settings">Settings</a>
        </nav>
      </main>
    </div>
  );
}

test('within limits search scope', () => {
  render(<Dashboard />);
  
  const header = screen.getByRole('banner');
  const headerNav = within(header).getByRole('navigation');
  const homeLink = within(headerNav).getByRole('link', { name: 'Home' });
  
  const main = screen.getByRole('main');
  const mainNav = within(main).getByRole('navigation');
  const dashboardLink = within(mainNav).getByRole('link', { name: 'Dashboard' });
});

链式查询 #

jsx
function NestedComponents() {
  return (
    <section aria-label="Products">
      <article aria-label="Product 1">
        <button>Add to Cart</button>
      </article>
      <article aria-label="Product 2">
        <button>Add to Cart</button>
      </article>
    </section>
  );
}

test('chained queries', () => {
  render(<NestedComponents />);
  
  const products = screen.getByRole('region', { name: 'Products' });
  const product1 = within(products).getByRole('article', { name: 'Product 1' });
  const addToCartBtn = within(product1).getByRole('button', { name: 'Add to Cart' });
});

高级匹配 #

正则表达式详解 #

jsx
function PriceList() {
  return (
    <ul>
      <li>Apple: $1.99</li>
      <li>Banana: $0.99</li>
      <li>Orange: $2.49</li>
    </ul>
  );
}

test('advanced regex patterns', () => {
  render(<PriceList />);
  
  // 匹配价格格式
  screen.getByText(/\$\d+\.\d{2}/);
  
  // 匹配特定水果
  screen.getByText(/Apple.*\$\d+\.\d{2}/);
  
  // 不区分大小写
  screen.getByText(/apple/i);
  
  // 多行匹配
  screen.getByText(/Apple[\s\S]*Banana/);
  
  // 使用 RegExp 构造函数
  const price = 1.99;
  screen.getByText(new RegExp(`\\$${price}`));
});

函数匹配器 #

jsx
function UserList({ users }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          {user.name} - {user.email}
        </li>
      ))}
    </ul>
  );
}

test('function matcher for complex matching', () => {
  const users = [
    { id: 1, name: 'John Doe', email: 'john@example.com' },
    { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
  ];
  
  render(<UserList users={users} />);
  
  // 使用函数进行复杂匹配
  screen.getByText((content, element) => {
    return content.includes('John') && content.includes('john@example.com');
  });
  
  // 匹配特定格式
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  screen.getByText((content) => {
    const parts = content.split(' - ');
    return parts.length === 2 && emailPattern.test(parts[1]);
  });
});

错误调试 #

查看可用元素 #

jsx
function DebugExample() {
  return (
    <div>
      <h1>Title</h1>
      <button>Click</button>
    </div>
  );
}

test('debug available elements', () => {
  render(<DebugExample />);
  
  // 打印所有可用角色
  screen.getByRole('button');
  
  // 如果找不到元素,错误消息会显示可用的角色和元素
  // screen.getByRole('textbox');
  // Error: Unable to find an element with the role: textbox
  //
  // Here are the available roles:
  //   heading:
  //   Name "Title":
  //   <h1 />
  //   button:
  //   Name "Click":
  //   <button />
});

使用 debug 输出 DOM #

jsx
test('debug DOM structure', () => {
  const { debug } = render(<DebugExample />);
  
  // 打印整个 DOM
  debug();
  
  // 打印特定元素
  debug(screen.getByRole('button'));
});

使用 logTestingPlaygroundURL #

jsx
test('generate playground URL', () => {
  render(<DebugExample />);
  
  // 生成可交互的 playground URL
  screen.logTestingPlaygroundURL();
  
  // 输出: Open this URL in your browser...
  // 可以在浏览器中交互式地探索查询
});

最佳实践 #

选择正确的查询 #

jsx
// ✅ 最佳 - 使用语义化查询
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText(/email/i);

// ✅ 良好 - 使用文本查询
screen.getByText('Welcome');

// ⚠️ 可接受 - 占位符查询
screen.getByPlaceholderText('Search');

// ❌ 避免 - testId 查询(除非必要)
screen.getByTestId('submit-button');

查询方法选择指南 #

场景 推荐查询
按钮、链接 getByRole(‘button’) / getByRole(‘link’)
表单输入 getByLabelText()
搜索框 getByPlaceholderText()
标题、段落 getByText()
图片 getByAltText() / getByRole(‘img’)
下拉选择值 getByDisplayValue()
无语义元素 getByTestId()

下一步 #

现在你已经深入了解了各种查询方法,接下来学习 用户交互与事件 掌握如何模拟用户操作!

最后更新:2026-03-28