第 3 章 · 定位器 Locator 与严格模式
本章目标:掌握 Locator"随时重新查找"的设计理念与各内置定位器,理解严格模式为什么能救你的测试,学会用 filter 链精确定位列表项。
3.1 Locator vs ElementHandle:一个重渲染就懂的区别
Locator 是 Playwright 自动等待与可重试能力的核心:它不代表某个具体 DOM 节点,而代表"如何找到节点"的查询。每次对 Locator 执行操作,都会在当下重新执行一次查找:
locator = page.get_by_role("button", name="Sign in")
locator.hover() # 此刻查找一次
locator.click() # 又重新查找一次 —— 用的是最新鲜的 DOM如果两次操作之间页面发生了重渲染(前端框架的日常),Locator 会自动指向新元素。而老式的 ElementHandle 在创建时就把节点"抓死",DOM 一变它就成了孤儿,操作直接报错 Element is not attached to the DOM:
# ❌ 反模式:handle 是快照,重渲染后失效
handle = page.query_selector("#submit")
# ✅ 正解:locator 是活查询
page.locator("#submit").click()记住一句话
测试代码里应该几乎见不到 query_selector / $。见到 ElementHandle 通常意味着踩进了反模式。
3.2 内置定位器全家桶
官方推荐优先使用面向用户属性的定位器,可测性和语义都更好:
# 按无障碍角色 + 可访问名称定位(首选)
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,可以全局改成自己的属性名:
playwright.selectors.set_test_id_attribute("data-pw")CSS/XPath 依然可用(page.locator("css=...")、自动识别 //button),但长链路如 #tsf > div:nth-child(2) > ... 是官方点名的坏实践——DOM 结构一变就碎。
3.3 严格模式:把歧义扼杀在运行前
传统框架遇到匹配多个元素时默默点第一个,错误被吞掉。Playwright 默认开启严格模式:任何操作/断言若解析出多个元素,立即抛错:
# 页面有两个 <button> 时:
page.locator("button").click()
# TimeoutError: strict mode violation: locator resolved to 2 elements这不是麻烦而是保护——"我以为点的那个"和"实际点的那个"不一致是 flaky 测试的经典来源。要主动收窄范围而不是碰运气:
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 链式过滤:列表定位的正解
商品列表里想点第二个商品的购买按钮,正确姿势是把"行"先筛出来再下钻:
# 文本过滤:找包含 "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,下列说法错误的是?
🛠️ 动手实践
- 打开任意有搜索框的站点,分别用
get_by_placeholder与get_by_role("searchbox")定位并输入文字。 - 构造一个本地 HTML 页面(含两个同名按钮),复现 strict mode violation,然后分别用
.first和filter(visible=True)解决它。 - 写一个含 5 个
<li>商品(其中 2 个标着 Out of stock)的页面,断言"有货商品数量为 3"。