首页 / Playwright 入门教程 / 链式定位与过滤

Playwright 入门教程

链式定位与过滤

本教程共 59 篇 · 第 14 篇 · 更新于 2026-08-04 · 约 9 分钟阅读

链式定位filternthfirstlastandor

14. 链式定位与过滤

本节目标:学会把一个大定位器逐步缩窄到目标元素,掌握 filter 按文字/子元素过滤,会用 nth/first/last 取第几个,并了解 and/or 的交集并集用法。

为什么要链式

真实页面里到处是成组的相似元素:商品列表、表格行、卡片。一个定位器往往同时命中好几个,直接操作就会撞上严格模式(Strict Mode)报错。

解法不是硬写一长串 CSS,而是把定位器一层层缩窄getByRolegetByText 这些创建定位器的方法,在 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();
Warning

filter 里的定位器从「外层匹配到的节点」往下找,不是从 <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 个
Warning

first/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 操作元素。