news 2026/9/5 17:23:42

Playwright LocatorAssertions 详解:expect 定位器断言的完整 API 与自动等待原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Playwright LocatorAssertions 详解:expect 定位器断言的完整 API 与自动等待原理

Playwright LocatorAssertions 详解:expect 定位器断言的完整 API 与自动等待原理

【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright

本文基于 Playwright 官方 API 文档 class-locatorassertions.md 整理并深入源码印证。读完后你将掌握:LocatorAssertions全部断言方法(toBeAttachedtoBeCheckedtoHaveTexttoHaveScreenshottoMatchAriaSnapshot等)的语义、参数与多语言用法;not取反与timeout/signal通用选项的底层实现;以及自动轮询等待与失败错误信息的产生机制。

一、LocatorAssertions 是什么

LocatorAssertions类(自 v1.17 引入)提供了一组用于断言 Locator 状态的断言方法,是@playwright/testexpect(locator)返回值的完整方法集合。文档给出的最小示例如下(JavaScript / Java / Python / C# 四种语言均可用):

import { test, expect } from '@playwright/test'; test('status becomes submitted', async ({ page }) => { await page.getByRole('button').click(); await expect(page.locator('.status')).toHaveText('Submitted'); });
# Python 同步 API from playwright.sync_api import Page, expect def test_status_becomes_submitted(page: Page): page.get_by_role("button").click() expect(page.locator(".status")).to_have_text("Submitted")
using Microsoft.Playwright; using Microsoft.Playwright.MSTest; [TestMethod] public async Task StatusBecomesSubmitted() { await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync(); await Expect(Page.Locator(".status")).ToHaveTextAsync("Submitted"); }

Java 侧通过com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat静态导入获得等价能力,例如assertThat(page.locator(".status")).hasText("Submitted")

与 Jest 原生断言不同,这些断言自动轮询等待直到条件成立或超时,而不是立即失败。文档中toBeAttached的一个典型场景是:点击后弹出包含Hidden text文本的模态框——断言会等到元素真正挂到 DOM 上:

await expect(page.getByText('Hidden text')).toBeAttached();

二、源码实现:断言是如何自动等待的

LocatorAssertions的方法在测试库侧统一实现在 matchers.ts 中。以toBeAttached为例(该文件第 81 行起):

export function toBeAttached( this: ExpectMatcherStateInternal, locator: LocatorEx, options?: { attached?: boolean, timeout?: number, signal?: AbortSignal }, ) { const attached = !options || options.attached === undefined || options.attached; const expected = attached ? 'attached' : 'detached'; const arg = attached ? '' : '{ attached: false }'; return toBeTruthy.call(this, 'toBeAttached', locator, 'Locator', expected, arg, async (isNot, timeout, signal) => { return await locator._expect(attached ? 'to.be.attached' : 'to.be.detached', { isNot, timeout, signal, title: this.title }); }, options); }

可以看到所有状态类断言(toBeAttachedtoBeCheckedtoBeDisabled…)都复用同一个核心函数toBeTruthy(见 toBeTruthy.ts):

  1. 超时解析const timeout = options.timeout ?? this.timeout;——单次调用的timeout参数优先于全局配置。全局配置的取值链在 expect.ts 中实现:const timeout = info.timeout ?? expectConfig().timeout ?? defaultExpectTimeout;,即调用参数 →expect.configure({ timeout })→ 测试配置expect.timeout→ 默认值
  2. 轮询查询:把期望条件(如'to.be.attached')序列化后通过locator._expect(...)下发到浏览器侧执行,并反复重试直到deadline
  3. 结果判定if (pass === !this.isNot)判断not修饰下的通过与否;
  4. 失败诊断:失败时由formatMatcherMessage生成包含LocatorExpectedReceivedTimeoutCall log的完整错误信息。测试 expect-timeout.spec.ts 精确验证了这些错误输出,例如:
expect(locator).toHaveText(expected) failed Locator: locator('div') Expected: "hey" Received: "Text content" Timeout: 1000ms Call log:

这套机制意味着:断言不通过时,你拿到的不是"那一刻的快照",而是超时前反复重试后仍未满足的证据,这是 Playwright 断言稳定性的核心来源。

三、not取反:断言相反条件

LocatorAssertions.not(v1.20 起,Java/JS/C# 支持;Python 提供独立的not_to_*方法族)将断言检查改为相反条件。文档示例:

await expect(locator).not.toContainText('error');
assertThat(locator).not().containsText("error");
await Expect(locator).Not.ToContainTextAsync("error");

在 Python 中则写成expect(locator).not_to_contain_text("error")not的实现正是上面源码中isNot标志:查询条件被原样下发(locator._expect(..., { isNot, ... })),最终由pass === !this.isNot完成取反判定——也就是说取反发生在"判定层",等待轮询逻辑完全复用。

文档同时为每个断言提供了对应的NotTo*变体说明(如NotToBeAttachedNotToHaveCountNotToMatchAriaSnapshot),语义均为"对应正向断言的相反条件",并各自支持timeout等选项。

四、状态类断言(toBe*)

以下断言检查元素的"状态",均支持timeout,JS 侧自 v1.62 起还支持signal(传入AbortSignal可中断等待)。

断言语义额外选项起始版本
toBeAttached元素已连接到 Document 或 ShadowRoot(等价Node.isConnectedattached(可断言已脱离)v1.33
toBeChecked复选框/单选已选中checked(v1.18)、indeterminate(v1.50,仅 checkbox/radio,与checked互斥)v1.20
toBeDisabled元素具有disabled属性或被aria-disabled禁用v1.20
toBeEditable元素可编辑editable(v1.26)v1.20
toBeEmpty可编辑元素为空,或 DOM 节点无文本v1.20
toBeEnabled元素处于启用状态enabled(v1.26)v1.20
toBeFocused元素持有焦点v1.20
toBeHiddenLocator 不解析到任何节点,或解析到不可见节点v1.20
toBeInViewport元素与视口相交(基于 Intersection Observer)ratio(默认0,表示任意正比例相交即可)v1.31
toBeVisible元素已挂载且可见visible(v1.26)v1.20

toBeChecked 的三种状态

const locator = page.getByLabel('Subscribe to newsletter'); await expect(locator).toBeChecked(); // 默认断言选中 await expect(locator).toBeChecked({ checked: false }); // 断言未选中 await expect(locator).toBeChecked({ indeterminate: true }); // 断言半选(indeterminate)

文档明确:checked选项与indeterminate: true不能同时设置。

toBeDisabled 的适用范围

文档特别提醒:只有原生控件buttoninputselecttextareaoptionoptgroup)能通过disabled属性被禁用,其他元素上的disabled属性会被浏览器忽略;aria-disabled则可用于任意元素。

toBeInViewport 的比例控制

const locator = page.getByRole('button'); await expect(locator).toBeInViewport(); // 至少部分相交 await expect(locator).not.toBeInViewport(); // 完全在视口外 await expect(locator).toBeInViewport({ ratio: 0.5 }); // 至少一半相交

ratio默认0,表示"任意正比例相交"即通过;设为1则要求元素完全进入视口。

toBeVisible 与列表选择器

toBeVisible要求 Locator 解析到恰好一个可见节点。对列表,文档给出两个惯用组合:first()断言"至少一项可见";or()+first()断言"两个候选中至少一个可见":

// A specific element is visible. await expect(page.getByText('Welcome')).toBeVisible(); // At least one item in the list is visible. await expect(page.getByTestId('todo-item').first()).toBeVisible(); // At least one of the two elements is visible, possibly both. await expect( page.getByRole('button', { name: 'Sign in' }) .or(page.getByRole('button', { name: 'Sign up' })) .first() ).toBeVisible();

注意toBeHiddentoBeVisible的边界差异:toBeHidden允许 Locator 完全不解析到节点(元素尚未渲染也算隐藏),而toBeVisible要求元素既挂载又可见

五、文本类断言:toHaveText 与 toContainText

toHaveText:精确匹配

toHaveText(v1.18 起)断言元素的完整文本内容(包含所有嵌套元素计算的文本),支持字符串、正则和数组:

const locator = page.locator('.title'); await expect(locator).toHaveText(/Welcome, Test User/); await expect(locator).toHaveText(/Welcome, .*/);

关键细节(文档Details节原文):

  • 字符串期望值:Playwright 会对实际文本与期望文本同时做空白与换行归一化后再匹配;
  • 正则期望值:实际文本按原样(as is)匹配,不做归一化;
  • 数组期望值:要求 Locator 解析出的元素数量与数组长度严格相等,且逐元素按顺序匹配。文档给出的正反例(以三个<li>的列表为例):
// ✓ Has the right items in the right order await expect(page.locator('ul > li')).toHaveText(['Text 1', 'Text 2', 'Text 3']); // ✖ Wrong order await expect(page.locator('ul > li')).toHaveText(['Text 3', 'Text 2', 'Text 1']); // ✖ Last item does not match await expect(page.locator('ul > li')).toHaveText(['Text 1', 'Text 2', 'Text']); // ✖ Locator points to the outer list element, not to the list items await expect(page.locator('ul')).toHaveText(['Text 1', 'Text 2', 'Text 3']);

选项:

  • ignoreCase(v1.23 起):忽略大小写;
  • useInnerText:改为使用element.innerText而非element.textContent取文本(区别在于innerText会考虑 CSS 可见性,如display: none的内容不计入);
  • timeout/signal

toContainText:子串匹配

toContainText要求元素包含给定文本(子串/正则),嵌套元素同样计入文本计算:

const locator = page.locator('.title'); await expect(locator).toContainText('substring'); await expect(locator).toContainText(/\d messages/);
import re locator = page.locator('.title') expect(locator).to_contain_text("substring") expect(locator).to_contain_text(re.compile(r"\d messages"))

数组语义与 toHaveText 不同toContainText的数组是"子集 + 保序"匹配——Locator 解析出的元素列表中存在一个子集,其元素依次包含期望数组中的文本即可,不要求数量相等。文档给出的示例(列表含 "Item Text 1/2/3"):

// ✓ Contains the right items in the right order await expect(page.locator('ul > li')).toContainText(['Text 1', 'Text 3']); // ✖ Wrong order await expect(page.locator('ul > li')).toContainText(['Text 3', 'Text 2']); // ✖ No item contains this text await expect(page.locator('ul > li')).toContainText(['Some 33']); // ✖ Locator points to the outer list element, not to the list items await expect(page.locator('ul')).toContainText(['Text 3']);

这与 matchers.ts 中serializeExpectedTextValues辅助函数相呼应:文本期望被序列化为{ string, regexSource, regexFlags, matchSubstring, ignoreCase, normalizeWhiteSpace }结构下发到浏览器侧执行,其中matchSubstring正是toContainTexttoHaveText的分水岭。

六、DOM 与属性类断言

toHaveAttribute / toHaveId / toHaveClass / toContainClass

  • toHaveAttribute(name, value):断言元素具有给定属性及值;value支持字符串或正则。JS 侧自 v1.39 起可省略value只断言属性存在;Python 侧自 v1.62 起同样支持省略:
const locator = page.locator('input'); await expect(locator).toHaveAttribute('type', 'text'); // Assert attribute existence. await expect(locator).toHaveAttribute('disabled'); await expect(locator).not.toHaveAttribute('open');
locator = page.locator("input") expect(locator).to_have_attribute("type", "text") expect(locator).to_have_attribute("disabled") # 仅断言存在 expect(locator).not_to_have_attribute("readonly") # 断言不存在

ignoreCase(v1.40 起)对属性值比较生效。

  • toHaveId(id):断言元素的 DOM id。
  • toHaveClass(expected)完整匹配元素的class属性(字符串必须逐字相等),或正则,或数组(数组按元素逐一对应整值匹配)。
  • toContainClass(expected)(v1.52 起):包含匹配——期望值按空格拆分的每个类名都必须出现在元素的classList中,顺序不限:
<div class='middle selected row' id='component'></div>
const locator = page.locator('#component'); await expect(locator).toContainClass('middle selected row'); await expect(locator).toContainClass('selected'); await expect(locator).toContainClass('row middle');

传数组时同样按"定位到的元素列表与期望列表一一对应"逐元素包含匹配:

await expect(page.locator('.list > .component')).toContainClass(['inactive', 'active', 'inactive']);

toHaveCSS / toHaveJSProperty / toHaveRole

  • toHaveCSS(name, value):断言计算样式(computed style)。v1.60 起支持pseudo选项('before' | 'after'),从伪元素读取计算样式。
  • toHaveJSProperty(name, value):断言元素上的 JavaScript 属性,值可以是原始类型或可序列化的普通 JS 对象(序列化在浏览器侧完成,因此不能含函数、循环引用等)。
  • toHaveRole(role)(v1.44 起):断言元素的 ARIA role。文档特别指出:role 按字符串精确匹配,不遵循 ARIA 角色继承——例如元素实际角色是switchcheckbox的子类)时,断言checkbox会失败。
const locator = page.getByTestId('save-button'); await expect(locator).toHaveRole('button');

toHaveCount / toHaveValue / toHaveValues

  • toHaveCount(count):断言 Locator 解析到确切数量的 DOM 节点。
  • toHaveValue(value):断言输入框当前值,支持字符串/正则:
const locator = page.locator('input[type=number]'); await expect(locator).toHaveValue(/[0-9]/);
  • toHaveValues(values)(v1.23 起):仅适用于多选<select multiple>(或 combobox),断言当前选中的 option 值集合,元素按顺序匹配:
// 给定 <select id="favorite-colors" multiple>,选项值为 R/G/B const locator = page.locator('id=favorite-colors'); await locator.selectOption(['R', 'G']); await expect(locator).toHaveValues([/R/, /G/]);

七、无障碍断言:AccessibleName / Description / ErrorMessage 与 toMatchAriaSnapshot

这四个无障碍断言让测试直接验证Accessibility Tree层面用户(含屏幕阅读器)看到的内容,而非原始 HTML:

断言检查对象起始版本
toHaveAccessibleNameaccessible name(由标签文本、aria-label、alt 等按 AccName 规范计算)v1.44
toHaveAccessibleDescriptionaccessible description(aria-describedby等)v1.44
toHaveAccessibleErrorMessagearia-errormessage引用的错误信息v1.50
toHaveRoleARIA role(字符串精确匹配)v1.44

用法(JS/Java/Python/C# 示例均与上文一致,此处以 JS 为例):

await expect(page.getByTestId('save-button')).toHaveAccessibleDescription('Save results to disk'); await expect(page.getByTestId('username-input')).toHaveAccessibleErrorMessage('Username is required.'); await expect(page.getByTestId('save-button')).toHaveAccessibleName('Save to disk');

前三个均支持ignoreCase(v1.44/v1.50 起)与timeout

toMatchAriaSnapshot(v1.49 起)更进一步:断言目标元素匹配一份无障碍快照(YAML 描述的子树),是 Playwright ARIA 快照体系的一部分(参见 aria-snapshots.md):

await page.goto('https://demo.playwright.dev/todomvc/'); await expect(page.locator('body')).toMatchAriaSnapshot(` - heading "todos" - textbox "What needs to be done?" `);
page.navigate("https://demo.playwright.dev/todomvc/"); assertThat(page.locator("body")).matchesAriaSnapshot(""" - heading "todos" - textbox "What needs to be done?" """);

JS 侧自 v1.50 起还支持无参重载:快照文件以.aria.yml形式存储到由配置文件expect.toMatchAriaSnapshot.pathTemplatesnapshotPathTemplate决定的位置,name选项可指定快照名,缺省时自动生成顺序名:

await expect(page.locator('body')).toMatchAriaSnapshot(); await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'body.aria.yml' });

其 JS 实现入口在 toMatchAriaSnapshot.ts,核心断言方法(如文本类)的轮询与比对逻辑则集中在 toMatchText.ts。

八、toHaveScreenshot:定位器级视觉回归

toHaveScreenshot(v1.23 起,仅 JS 测试运行器支持)是定位器级截图断言。文档描述了其核心策略:先连续截图直到两张结果一致(排除动画干扰),再将最后一张与期望快照比对:

const locator = page.getByRole('button'); await expect(locator).toHaveScreenshot('image.png'); // Store the snapshot in the WebP format. await expect(locator).toHaveScreenshot('image.webp');

快照名必须带.png.webp扩展名,两者均为无损格式;无参重载默认以 PNG 格式命名存储。完整选项表:

选项说明起始版本
name快照名,.png.webpv1.23
timeout/signal等待超时 / 取消信号v1.23 / v1.62
animations截图时是否禁用 CSS 动画(默认disabledv1.23
caret文本光标处理方式v1.23
mask/maskColor遮挡动态区域及遮挡颜色v1.23 / v1.35
stylePath注入自定义样式文件v1.41
omitBackground省略页面背景v1.23
scalecssdevice比例(默认cssv1.23
maxDiffPixels允许差异的最大像素数v1.23
maxDiffPixelRatio允许差异的最大像素比例v1.23
threshold单像素颜色差异容忍阈值v1.23

截图断言与文本/状态断言一样支持自动重试:首次失败会触发测试运行器的"更新快照"工作流(--update-snapshots),快照基线与测试用例按目录结构存放——仓库中 test-snapshots-js.md 对快照目录与更新流程有完整说明,可延伸阅读。

九、通用选项与失败排查

汇总文档中反复出现的选项约定:

  1. timeout:所有断言均可传入,单位毫秒。优先级为调用参数 >expect.configure({ timeout })> 配置expect.timeout> 内置默认值(源码见 expect.ts 第 392 行的取值链)。
  2. signal(JS,v1.62 起):标准AbortSignal,用于与Promise.race、测试超时机制集成,取消进行中的断言等待。
  3. not(Java/JS/C#)与not_to_*(Python):对任意断言取反,实现上通过isNot标志在判定层生效,等待逻辑不变。
  4. 状态类断言的布尔选项(checked/enabled/visible/editable/attached等)允许"显式断言反向状态",与not等价但语义更明确,例如toBeChecked({ checked: false })
  5. 失败时:错误信息固定包含Expected/Received(如有)/Timeout/Call log四段,测试 expect-timeout.spec.ts 与 expect-misc.spec.ts 锁定了这些输出格式;行为细节(如"element(s) not found"与"值不匹配"两种超时错误)分别验证了等待机制的两条路径。

十、多语言 API 对照速查

同一断言在四种语言中的命名规则(来自文档alias-java标注):

JS (expect(locator))Java (assertThat(locator))Python (expect(locator))C# (Expect(locator))
toBeAttachedisAttachedto_be_attached/not_to_be_attachedToBeAttachedAsync/Not.ToBeAttachedAsync
toBeCheckedisCheckedto_be_checkedToBeCheckedAsync
toBeVisibleisVisibleto_be_visibleToBeVisibleAsync
toBeInViewportisInViewportto_be_in_viewportToBeInViewportAsync
toHaveTexthasTextto_have_textToHaveTextAsync
toContainTextcontainsTextto_contain_textToContainTextAsync
toHaveCounthasCountto_have_countToHaveCountAsync
toHaveScreenshot—(JS 专有)
toMatchAriaSnapshotmatchesAriaSnapshotto_match_aria_snapshotToMatchAriaSnapshotAsync

参考路径

  • API 文档源文件:docs/src/api/class-locatorassertions.md
  • JS 断言实现:packages/playwright/src/matchers/matchers.ts、packages/playwright/src/matchers/toBeTruthy.ts、packages/playwright/src/matchers/expect.ts、packages/playwright/src/matchers/toMatchAriaSnapshot.ts
  • 行为测试:tests/page/expect-timeout.spec.ts、tests/page/expect-misc.spec.ts、tests/page/expect-to-have-text.spec.ts、tests/page/expect-to-have-accessible.spec.ts、tests/page/expect-boolean.spec.ts
  • 配套文档:docs/src/aria-snapshots.md、docs/src/actionability.md(toBeHidden/toBeVisible中 "visible" 的定义)、docs/src/test-snapshots-js.md

【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 17:21:38

OpenClaw 2.0六大升级:任务续跑、记忆分层与细粒度权限实战解析

刚把内部几个 Agent 任务从旧版脚本迁到 OpenClaw 2.0 上&#xff0c;整体跑了两周多。这个版本在安装方式、任务续跑、记忆、权限这几个方向上的改动&#xff0c;确实比一次普通小版本迭代要大&#xff0c;尤其是权限模型和记忆持久化&#xff0c;直接影响了项目里多 Agent 协…

作者头像 李华
网站建设 2026/9/5 17:21:33

SDWebImage异步图片加载实战:从第一行代码到生产可用

SDWebImage异步图片加载实战&#xff1a;从第一行代码到生产可用 【免费下载链接】SDWebImage Asynchronous image downloader with cache support as a UIImageView category 项目地址: https://gitcode.com/GitHub_Trending/sd/SDWebImage 做信息流页面时&#xff0c;…

作者头像 李华
网站建设 2026/9/5 17:13:53

AERIS-10开源相控阵雷达上手指南:10.5GHz PLFM系统从仓库到跑通

AERIS-10开源相控阵雷达上手指南&#xff1a;10.5GHz PLFM系统从仓库到跑通 【免费下载链接】PLFM_RADAR Open-source, low-cost 10.5 GHz PLFM phased array RADAR system 项目地址: https://gitcode.com/GitHub_Trending/pl/PLFM_RADAR AERIS-10 是一个开源的 10.5 GH…

作者头像 李华