Skip to content

第 3 章 · 定位器 Locator 与严格模式

本章目标:掌握 Locator"随时重新查找"的设计理念与各内置定位器,理解严格模式为什么能救你的测试,学会用 filter 链精确定位列表项。

3.1 Locator vs ElementHandle:一个重渲染就懂的区别

Locator 是 Playwright 自动等待与可重试能力的核心:它不代表某个具体 DOM 节点,而代表"如何找到节点"的查询。每次对 Locator 执行操作,都会在当下重新执行一次查找:

python
locator = page.get_by_role("button", name="Sign in")
locator.hover()   # 此刻查找一次
locator.click()   # 又重新查找一次 —— 用的是最新鲜的 DOM

如果两次操作之间页面发生了重渲染(前端框架的日常),Locator 会自动指向新元素。而老式的 ElementHandle 在创建时就把节点"抓死",DOM 一变它就成了孤儿,操作直接报错 Element is not attached to the DOM

python
# ❌ 反模式:handle 是快照,重渲染后失效
handle = page.query_selector("#submit")

# ✅ 正解:locator 是活查询
page.locator("#submit").click()

记住一句话

测试代码里应该几乎见不到 query_selector / $。见到 ElementHandle 通常意味着踩进了反模式。

3.2 内置定位器全家桶

官方推荐优先使用面向用户属性的定位器,可测性和语义都更好:

python
# 按无障碍角色 + 可访问名称定位(首选)
page.get_by_role("button", name="Sign in").click()
page.get_by_role("checkbox", name="Subscribe").check()

# 按 label 定位表单控件
page.get_by_label("Password").fill("secret")

# 按占位符
page.get_by_placeholder("name@example.com").fill("me@test.com")

# 按文本(适合 div/span 等非交互元素;支持正则与 exact=True)
expect(page.get_by_text(re.compile("welcome, john", re.IGNORECASE))).to_be_visible()

# 按图片 alt 文本、按 title 属性
page.get_by_alt_text("playwright logo").click()

# 按 test id(默认 data-testid 属性)
page.get_by_test_id("directions").click()

选择优先级建议:get_by_role → 表单用 get_by_label → 非交互文本用 get_by_text → 团队约定了 test id 就用 get_by_test_id

test id 是最抗变的锚点——文案改版、结构调整都不影响。默认读 data-testid,可以全局改成自己的属性名:

python
playwright.selectors.set_test_id_attribute("data-pw")

CSS/XPath 依然可用(page.locator("css=...")、自动识别 //button),但长链路如 #tsf > div:nth-child(2) > ... 是官方点名的坏实践——DOM 结构一变就碎。

3.3 严格模式:把歧义扼杀在运行前

传统框架遇到匹配多个元素时默默点第一个,错误被吞掉。Playwright 默认开启严格模式:任何操作/断言若解析出多个元素,立即抛错:

python
# 页面有两个 <button> 时:
page.locator("button").click()
# TimeoutError: strict mode violation: locator resolved to 2 elements

这不是麻烦而是保护——"我以为点的那个"和"实际点的那个"不一致是 flaky 测试的经典来源。要主动收窄范围而不是碰运气:

python
page.locator("button", has_text="Save").first.click()      # 明确取第一个
page.get_by_role("listitem").nth(1)                        # 明确取第 n 个(谨慎用)
page.locator("button").filter(visible=True).click()        # 只匹配可见元素

3.4 filter 链式过滤:列表定位的正解

商品列表里想点第二个商品的购买按钮,正确姿势是把"行"先筛出来再下钻:

python
# 文本过滤:找包含 "Product 2" 的列表项,再在其中找按钮
page.get_by_role("listitem").filter(
    has_text="Product 2"
).get_by_role("button", name="Add to cart").click()

# 子元素条件过滤:该行内必须存在指定 heading
page.get_by_role("listitem").filter(
    has=page.get_by_role("heading", name="Product 2")
).get_by_role("button", name="Add to cart").click()

# 反向过滤:排除缺货商品
expect(page.get_by_role("listitem").filter(has_not_text="Out of stock")).to_have_count(5)

注意:filter(has=...) 里的内层 locator 从被过滤元素开始向下匹配,不要写成从 document 根出发的选择器。多个 filter 可以链式叠加进一步收窄。

Shadow DOM 也无需特殊处理:除 XPath 外,所有定位器默认穿透 shadow root(closed 模式除外)。

3.5 本章小结

  • Locator = 活查询,每次操作重新解析;ElementHandle = 死快照,重渲染即失效;
  • 定位器优先级:role → label/text → placeholder/alt/title → test id → CSS/XPath 兜底;
  • 严格模式让"匹配多个元素"直接报错,逼你写出无歧义定位;
  • 列表场景用 filter(has_text= / has= / has_not_text=) 链式下钻;
  • test id 最抗变,可用 selectors.set_test_id_attribute() 自定义属性名。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 页面发生重渲染后,以下哪种写法仍能正常工作?

2. locator 匹配到了 3 个元素并执行 click(),会发生什么?

3. 在商品列表中点击"包含 Product 2 的那一行"里的 Add to cart 按钮,正确的写法是?

4. 关于 get_by_test_id,下列说法错误的是?

🛠️ 动手实践

  1. 打开任意有搜索框的站点,分别用 get_by_placeholderget_by_role("searchbox") 定位并输入文字。
  2. 构造一个本地 HTML 页面(含两个同名按钮),复现 strict mode violation,然后分别用 .firstfilter(visible=True) 解决它。
  3. 写一个含 5 个 <li> 商品(其中 2 个标着 Out of stock)的页面,断言"有货商品数量为 3"。

进入第 4 章:自动等待与 Actionability