链式定位与过滤
本教程共 59 篇 · 第 14 篇 · 更新于 2026-08-04 · 约 9 分钟阅读
本节目标:学会把一个大定位器逐步缩窄到目标元素,掌握 filter 按文字/子元素过滤,会用 nth/first/last 取第几个,并了解 and/or 的交集并集用法。
为什么要链式
真实页面里到处是成组的相似元素:商品列表、表格行、卡片。一个定位器往往同时命中好几个,直接操作就会撞上严格模式(Strict Mode)报错。
解法不是硬写一长串 CSS,而是把定位器一层层缩窄。getByRole、getByText 这些创建定位器的方法,在 Locator(定位器)上也都有一份,所以能接在后面继续往下找。
用 .locator() 往下缩窄
在已有定位器里继续找子元素:
const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });
await product.getByRole('button', { name: 'Add to cart' }).click();
await expect(product).toHaveCount(1);
也可以把两个定位器「套娃」:
const dialog = page.getByTestId('settings-dialog');
const saveButton = page.getByRole('button', { name: 'Save' });
await dialog.locator(saveButton).click(); // 在 dialog 里找那个 Save
用 .filter() 按文字过滤
filter({ hasText }) 在元素内部(含后代节点)搜文字。默认是子串匹配、大小写不敏感,也接受正则。
// 点第二个商品卡片里的「加入购物车」
await page
.getByRole('listitem')
.filter({ hasText: 'Product 2' })
.getByRole('button', { name: 'Add to cart' })
.click();
反向过滤也有:按「不含文字」筛。
// 统计不含「缺货」的商品,期望有 5 个
await expect(
page.getByRole('listitem').filter({ hasNotText: 'Out of stock' })
).toHaveCount(5);
用 .filter() 按子元素过滤
filter({ has }) 只保留「内部含某个匹配子元素」的那些。注意:里面的定位器是相对于外层去找的,不是从文档根找。
await page
.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) })
.getByRole('button', { name: 'Add to cart' })
.click();
Warningfilter 里的定位器从「外层匹配到的节点」往下找,不是从
<body>找。下面这样写是错的:它从<ul>找,但<ul>在外层<li>之外,匹配不到。// ✖ 错误示范 page.getByRole('listitem') .filter({ has: page.getByRole('list').getByText('Product 2') })
还能按「不含某子元素」过滤:filter({ hasNot: ... })。
用 .filter() 只留可见元素
有一类页面很烦人:DOM 里塞了一堆隐藏的模板节点,或者列表用 display: none 做筛选。这些节点你定位得到,用户却看不见,数量一断言就对不上。
filter({ visible: true }) 专治这个:
// 忽略隐藏的待办项,只数看得见的
const todoItems = page.getByTestId('todo-item').filter({ visible: true });
await expect(todoItems).toHaveCount(3);
Tip这个选项从 1.51 起提供,1.62.x 里可以放心用。以前只能写一长串 CSS 排除隐藏节点,现在一个参数解决。
叠加多个过滤条件
多个 filter 可以串起来,越收越精:
await page
.getByRole('listitem')
.filter({ hasText: 'Mary' })
.filter({ has: page.getByRole('button', { name: 'Say goodbye' }) })
.screenshot({ path: 'screenshot.png' });
取第几个:nth / first / last
当只能靠顺序区分时,用这几个。序号从 0 开始。
const banana = page.getByRole('listitem').nth(1); // 第 2 个
const first = page.getByRole('listitem').first(); // 第 1 个
const last = page.getByRole('listitem').last(); // 最后 1 个
Warningfirst/last/nth 不推荐常用。页面一变,序号对上的可能完全是另一个元素。能用文字、test id 唯一定位就别用序号。它们只在「确实别无他法」时兜底。
交集与并集:and / or
.and() 取同时满足两个定位器的元素:
const button = page.getByRole('button').and(page.getByTitle('Subscribe'));
.or() 取满足任意一个的元素,常用来等「要么这个、要么那个」:
const newEmail = page.getByRole('button', { name: 'New' });
const dialog = page.getByText('Confirm security settings');
await expect(newEmail.or(dialog).first()).toBeVisible();
if (await dialog.isVisible())
await page.getByRole('button', { name: 'Dismiss' }).click();
await newEmail.click();
Note若两个都满足,
.or()会匹配到多个,可能触发严格模式报错。此时加.first()只取一个。
小结
链式定位用 .locator() 往下缩,.filter() 按文字、子元素或可见性收,.nth() / .first() / .last() 按顺序取(慎用),.and() / .or() 做交集并集。
最容易记错的一点:filter 里的定位器是相对外层节点去找的,不是从文档根找。
下一章预告:定位 Frames 内嵌框架——用
frameLocator跨 iframe 操作元素。