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全部断言方法(toBeAttached、toBeChecked、toHaveText、toHaveScreenshot、toMatchAriaSnapshot等)的语义、参数与多语言用法;not取反与timeout/signal通用选项的底层实现;以及自动轮询等待与失败错误信息的产生机制。
一、LocatorAssertions 是什么
LocatorAssertions类(自 v1.17 引入)提供了一组用于断言 Locator 状态的断言方法,是@playwright/test中expect(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); }可以看到所有状态类断言(toBeAttached、toBeChecked、toBeDisabled…)都复用同一个核心函数toBeTruthy(见 toBeTruthy.ts):
- 超时解析:
const timeout = options.timeout ?? this.timeout;——单次调用的timeout参数优先于全局配置。全局配置的取值链在 expect.ts 中实现:const timeout = info.timeout ?? expectConfig().timeout ?? defaultExpectTimeout;,即调用参数 →expect.configure({ timeout })→ 测试配置expect.timeout→ 默认值; - 轮询查询:把期望条件(如
'to.be.attached')序列化后通过locator._expect(...)下发到浏览器侧执行,并反复重试直到deadline; - 结果判定:
if (pass === !this.isNot)判断not修饰下的通过与否; - 失败诊断:失败时由
formatMatcherMessage生成包含Locator、Expected、Received、Timeout与Call 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*变体说明(如NotToBeAttached、NotToHaveCount、NotToMatchAriaSnapshot),语义均为"对应正向断言的相反条件",并各自支持timeout等选项。
四、状态类断言(toBe*)
以下断言检查元素的"状态",均支持timeout,JS 侧自 v1.62 起还支持signal(传入AbortSignal可中断等待)。
| 断言 | 语义 | 额外选项 | 起始版本 |
|---|---|---|---|
toBeAttached | 元素已连接到 Document 或 ShadowRoot(等价Node.isConnected) | attached(可断言已脱离) | 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 |
toBeHidden | Locator 不解析到任何节点,或解析到不可见节点 | — | 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 的适用范围
文档特别提醒:只有原生控件(button、input、select、textarea、option、optgroup)能通过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();注意toBeHidden与toBeVisible的边界差异: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正是toContainText与toHaveText的分水岭。
六、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 角色继承——例如元素实际角色是switch(checkbox的子类)时,断言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:
| 断言 | 检查对象 | 起始版本 |
|---|---|---|
toHaveAccessibleName | accessible name(由标签文本、aria-label、alt 等按 AccName 规范计算) | v1.44 |
toHaveAccessibleDescription | accessible description(aria-describedby等) | v1.44 |
toHaveAccessibleErrorMessage | aria-errormessage引用的错误信息 | v1.50 |
toHaveRole | ARIA 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.pathTemplate和snapshotPathTemplate决定的位置,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或.webp | v1.23 |
timeout/signal | 等待超时 / 取消信号 | v1.23 / v1.62 |
animations | 截图时是否禁用 CSS 动画(默认disabled) | v1.23 |
caret | 文本光标处理方式 | v1.23 |
mask/maskColor | 遮挡动态区域及遮挡颜色 | v1.23 / v1.35 |
stylePath | 注入自定义样式文件 | v1.41 |
omitBackground | 省略页面背景 | v1.23 |
scale | css或device比例(默认css) | v1.23 |
maxDiffPixels | 允许差异的最大像素数 | v1.23 |
maxDiffPixelRatio | 允许差异的最大像素比例 | v1.23 |
threshold | 单像素颜色差异容忍阈值 | v1.23 |
截图断言与文本/状态断言一样支持自动重试:首次失败会触发测试运行器的"更新快照"工作流(--update-snapshots),快照基线与测试用例按目录结构存放——仓库中 test-snapshots-js.md 对快照目录与更新流程有完整说明,可延伸阅读。
九、通用选项与失败排查
汇总文档中反复出现的选项约定:
timeout:所有断言均可传入,单位毫秒。优先级为调用参数 >expect.configure({ timeout })> 配置expect.timeout> 内置默认值(源码见 expect.ts 第 392 行的取值链)。signal(JS,v1.62 起):标准AbortSignal,用于与Promise.race、测试超时机制集成,取消进行中的断言等待。not(Java/JS/C#)与not_to_*(Python):对任意断言取反,实现上通过isNot标志在判定层生效,等待逻辑不变。- 状态类断言的布尔选项(
checked/enabled/visible/editable/attached等)允许"显式断言反向状态",与not等价但语义更明确,例如toBeChecked({ checked: false })。 - 失败时:错误信息固定包含
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)) |
|---|---|---|---|
toBeAttached | isAttached | to_be_attached/not_to_be_attached | ToBeAttachedAsync/Not.ToBeAttachedAsync |
toBeChecked | isChecked | to_be_checked | ToBeCheckedAsync |
toBeVisible | isVisible | to_be_visible | ToBeVisibleAsync |
toBeInViewport | isInViewport | to_be_in_viewport | ToBeInViewportAsync |
toHaveText | hasText | to_have_text | ToHaveTextAsync |
toContainText | containsText | to_contain_text | ToContainTextAsync |
toHaveCount | hasCount | to_have_count | ToHaveCountAsync |
toHaveScreenshot | —(JS 专有) | — | — |
toMatchAriaSnapshot | matchesAriaSnapshot | to_match_aria_snapshot | ToMatchAriaSnapshotAsync |
参考路径
- 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),仅供参考