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

发布时间:2026/9/5 17:23:56
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全部断言方法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.notv1.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*以下断言检查元素的状态均支持timeoutJS 侧自 v1.62 起还支持signal传入AbortSignal可中断等待。断言语义额外选项起始版本toBeAttached元素已连接到 Document 或 ShadowRoot等价Node.isConnectedattached可断言已脱离v1.33toBeChecked复选框/单选已选中checkedv1.18、indeterminatev1.50仅 checkbox/radio与checked互斥v1.20toBeDisabled元素具有disabled属性或被aria-disabled禁用—v1.20toBeEditable元素可编辑editablev1.26v1.20toBeEmpty可编辑元素为空或 DOM 节点无文本—v1.20toBeEnabled元素处于启用状态enabledv1.26v1.20toBeFocused元素持有焦点—v1.20toBeHiddenLocator 不解析到任何节点或解析到不可见节点—v1.20toBeInViewport元素与视口相交基于 Intersection Observerratio默认0表示任意正比例相交即可v1.31toBeVisible元素已挂载且可见visiblev1.26v1.20toBeChecked 的三种状态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 与 toContainTexttoHaveText精确匹配toHaveTextv1.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]);选项ignoreCasev1.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 / toContainClasstoHaveAttribute(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) # 断言不存在ignoreCasev1.40 起对属性值比较生效。toHaveId(id)断言元素的 DOM id。toHaveClass(expected)完整匹配元素的class属性字符串必须逐字相等或正则或数组数组按元素逐一对应整值匹配。toContainClass(expected)v1.52 起包含匹配——期望值按空格拆分的每个类名都必须出现在元素的classList中顺序不限div classmiddle selected row idcomponent/divconst 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 / toHaveRoletoHaveCSS(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 / toHaveValuestoHaveCount(count)断言 Locator 解析到确切数量的 DOM 节点。toHaveValue(value)断言输入框当前值支持字符串/正则const locator page.locator(input[typenumber]); await expect(locator).toHaveValue(/[0-9]/);toHaveValues(values)v1.23 起仅适用于多选select multiple或 combobox断言当前选中的 option 值集合元素按顺序匹配// 给定 select idfavorite-colors multiple选项值为 R/G/B const locator page.locator(idfavorite-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.44toHaveAccessibleDescriptionaccessible descriptionaria-describedby等v1.44toHaveAccessibleErrorMessagearia-errormessage引用的错误信息v1.50toHaveRoleARIA 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);前三个均支持ignoreCasev1.44/v1.50 起与timeout。toMatchAriaSnapshotv1.49 起更进一步断言目标元素匹配一份无障碍快照YAML 描述的子树是 Playwright ARIA 快照体系的一部分参见 aria-snapshots.mdawait 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定位器级视觉回归toHaveScreenshotv1.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.23timeout/signal等待超时 / 取消信号v1.23 / v1.62animations截图时是否禁用 CSS 动画默认disabledv1.23caret文本光标处理方式v1.23mask/maskColor遮挡动态区域及遮挡颜色v1.23 / v1.35stylePath注入自定义样式文件v1.41omitBackground省略页面背景v1.23scalecss或device比例默认cssv1.23maxDiffPixels允许差异的最大像素数v1.23maxDiffPixelRatio允许差异的最大像素比例v1.23threshold单像素颜色差异容忍阈值v1.23截图断言与文本/状态断言一样支持自动重试首次失败会触发测试运行器的更新快照工作流--update-snapshots快照基线与测试用例按目录结构存放——仓库中 test-snapshots-js.md 对快照目录与更新流程有完整说明可延伸阅读。九、通用选项与失败排查汇总文档中反复出现的选项约定timeout所有断言均可传入单位毫秒。优先级为调用参数 expect.configure({ timeout }) 配置expect.timeout 内置默认值源码见 expect.ts 第 392 行的取值链。signalJSv1.62 起标准AbortSignal用于与Promise.race、测试超时机制集成取消进行中的断言等待。notJava/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))toBeAttachedisAttachedto_be_attached/not_to_be_attachedToBeAttachedAsync/Not.ToBeAttachedAsynctoBeCheckedisCheckedto_be_checkedToBeCheckedAsynctoBeVisibleisVisibleto_be_visibleToBeVisibleAsynctoBeInViewportisInViewportto_be_in_viewportToBeInViewportAsynctoHaveTexthasTextto_have_textToHaveTextAsynctoContainTextcontainsTextto_contain_textToContainTextAsynctoHaveCounthasCountto_have_countToHaveCountAsynctoHaveScreenshot—JS 专有——toMatchAriaSnapshotmatchesAriaSnapshotto_match_aria_snapshotToMatchAriaSnapshotAsync参考路径API 文档源文件docs/src/api/class-locatorassertions.mdJS 断言实现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.mdtoBeHidden/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),仅供参考