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