Playwright 断言验证 #
断言概述 #
Playwright 使用 expect 函数进行断言,所有断言都内置自动重试机制,会在超时时间内反复检查直到条件满足或超时。
自动重试机制 #
text
┌─────────────────────────────────────────────────────────────┐
│ expect 自动重试 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 断言失败时: │
│ ├── 等待一小段时间 │
│ ├── 重新获取元素状态 │
│ ├── 重新检查断言条件 │
│ └── 重复直到成功或超时 │
│ │
│ 默认超时: 5000ms │
│ │
└─────────────────────────────────────────────────────────────┘
基本用法 #
typescript
import { test, expect } from '@playwright/test';
test('基本断言', async ({ page }) => {
await page.goto('/');
// 元素断言
await expect(page.locator('.title')).toBeVisible();
// 文本断言
await expect(page.locator('.title')).toHaveText('Welcome');
// 页面断言
await expect(page).toHaveTitle(/Home/);
});
元素状态断言 #
可见性断言 #
typescript
test('可见性断言', async ({ page }) => {
// 元素可见
await expect(page.locator('.banner')).toBeVisible();
// 元素不可见
await expect(page.locator('.hidden')).not.toBeVisible();
await expect(page.locator('.hidden')).toBeHidden();
// 可见性选项
await expect(page.locator('.element')).toBeVisible({
timeout: 10000, // 自定义超时
});
});
启用/禁用断言 #
typescript
test('启用/禁用断言', async ({ page }) => {
// 元素已启用
await expect(page.getByRole('button')).toBeEnabled();
// 元素已禁用
await expect(page.getByRole('button')).toBeDisabled();
// 否定断言
await expect(page.getByRole('button')).not.toBeDisabled();
});
可编辑断言 #
typescript
test('可编辑断言', async ({ page }) => {
// 元素可编辑
await expect(page.getByRole('textbox')).toBeEditable();
// 元素只读
await expect(page.getByRole('textbox')).toBeEditable({ editable: false });
});
选中状态断言 #
typescript
test('选中状态断言', async ({ page }) => {
// 复选框已选中
await expect(page.getByRole('checkbox')).toBeChecked();
// 复选框未选中
await expect(page.getByRole('checkbox')).not.toBeChecked();
// 单选按钮已选中
await expect(page.getByRole('radio')).toBeChecked();
});
聚焦断言 #
typescript
test('聚焦断言', async ({ page }) => {
// 元素已聚焦
await expect(page.getByRole('textbox')).toBeFocused();
// 元素未聚焦
await expect(page.getByRole('textbox')).not.toBeFocused();
});
文本内容断言 #
文本匹配断言 #
typescript
test('文本匹配断言', async ({ page }) => {
// 精确匹配
await expect(page.locator('.title')).toHaveText('Welcome');
// 正则匹配
await expect(page.locator('.title')).toHaveText(/welcome/i);
// 部分匹配
await expect(page.locator('.title')).toContainText('Welcome');
// 多个元素文本
await expect(page.locator('.item')).toHaveText(['Item 1', 'Item 2', 'Item 3']);
// 使用选项
await expect(page.locator('.title')).toHaveText('Welcome', {
timeout: 10000,
useInnerText: true, // 使用 innerText
});
});
文本值断言 #
typescript
test('文本值断言', async ({ page }) => {
// 输入框的值
await expect(page.getByRole('textbox')).toHaveValue('test@example.com');
// 正则匹配
await expect(page.getByRole('textbox')).toHaveValue(/test@/);
// 空值
await expect(page.getByRole('textbox')).toHaveValue('');
// 多选下拉的值
await expect(page.getByRole('listbox')).toHaveValues(['option1', 'option2']);
});
Input Value 断言 #
typescript
test('Input Value 断言', async ({ page }) => {
const input = page.getByLabel('Email');
// 精确匹配
await expect(input).toHaveValue('test@example.com');
// 正则匹配
await expect(input).toHaveValue(/@example\.com$/);
// 空值
await expect(input).toBeEmpty();
});
属性断言 #
属性值断言 #
typescript
test('属性值断言', async ({ page }) => {
// 检查属性存在
await expect(page.locator('input')).toHaveAttribute('required');
// 检查属性值
await expect(page.locator('input')).toHaveAttribute('type', 'email');
// 正则匹配
await expect(page.locator('a')).toHaveAttribute('href', /\/products/);
// 多个属性
await expect(page.locator('input')).toHaveAttribute('placeholder', 'Enter email');
});
CSS 类断言 #
typescript
test('CSS 类断言', async ({ page }) => {
// 检查类名存在
await expect(page.locator('.card')).toHaveClass(/active/);
// 精确匹配类名
await expect(page.locator('.card')).toHaveClass('card active highlighted');
// 多个元素的类名
await expect(page.locator('.item')).toHaveClass(['item', 'item active', 'item']);
});
CSS 样式断言 #
typescript
test('CSS 样式断言', async ({ page }) => {
// 检查 CSS 属性
await expect(page.locator('.banner')).toHaveCSS('color', 'rgb(255, 0, 0)');
// 检查多个属性
await expect(page.locator('.banner')).toHaveCSS('background-color', 'rgb(0, 0, 255)');
// 使用正则
await expect(page.locator('.banner')).toHaveCSS('font-size', /\d+px/);
});
数量断言 #
typescript
test('数量断言', async ({ page }) => {
// 元素数量
await expect(page.locator('.item')).toHaveCount(5);
// 数量为 0
await expect(page.locator('.item')).toHaveCount(0);
// 动态数量
await expect(page.locator('.item')).toHaveCount(3, { timeout: 10000 });
});
页面断言 #
URL 断言 #
typescript
test('URL 断言', async ({ page }) => {
// 精确匹配
await expect(page).toHaveURL('https://example.com/dashboard');
// 正则匹配
await expect(page).toHaveURL(/dashboard/);
// 包含路径
await expect(page).toHaveURL(new RegExp('/dashboard'));
});
标题断言 #
typescript
test('标题断言', async ({ page }) => {
// 精确匹配
await expect(page).toHaveTitle('Dashboard - My App');
// 正则匹配
await expect(page).toHaveTitle(/Dashboard/);
// 包含文本
await expect(page).toHaveTitle(/My App$/);
});
快照断言 #
页面快照 #
typescript
test('页面快照', async ({ page }) => {
await page.goto('/landing');
// 全页快照
await expect(page).toHaveScreenshot('landing.png');
// 自动命名
await expect(page).toHaveScreenshot();
// 全页快照
await expect(page).toHaveScreenshot('full-landing.png', {
fullPage: true,
});
});
元素快照 #
typescript
test('元素快照', async ({ page }) => {
const card = page.locator('.product-card');
// 元素快照
await expect(card).toHaveScreenshot('card.png');
// 元素快照选项
await expect(card).toHaveScreenshot('card.png', {
maxDiffPixels: 100, // 最大差异像素
maxDiffPixelRatio: 0.1, // 最大差异比例
threshold: 0.2, // 对比阈值
animations: 'disabled', // 禁用动画
});
});
快照选项 #
typescript
test('快照选项', async ({ page }) => {
await expect(page).toHaveScreenshot('screenshot.png', {
// 差异容忍度
maxDiffPixels: 100,
maxDiffPixelRatio: 0.1,
threshold: 0.2,
// 屏蔽区域
mask: [page.locator('.dynamic-content')],
maskColor: '#ff0000',
// 裁剪区域
clip: { x: 0, y: 0, width: 800, height: 600 },
// 动画处理
animations: 'disabled', // 'allow' | 'disabled'
// 插图
caret: 'hide', // 'hide' | 'initial'
// 缩放
scale: 'css', // 'css' | 'device'
// 全页
fullPage: true,
// 超时
timeout: 10000,
});
});
断言选项 #
全局配置 #
typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
// 断言超时
timeout: 10000,
// 快照配置
toHaveScreenshot: {
maxDiffPixels: 100,
maxDiffPixelRatio: 0.1,
},
// 快照路径
snapshotPathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
},
});
单个断言配置 #
typescript
test('单个断言配置', async ({ page }) => {
// 自定义超时
await expect(page.locator('.element')).toBeVisible({
timeout: 10000,
});
// 自定义消息
await expect(page.locator('.element'), '元素应该可见').toBeVisible();
});
自定义断言消息 #
typescript
test('自定义断言消息', async ({ page }) => {
const title = page.locator('.title');
// 添加自定义消息
await expect(title, '标题应该显示欢迎信息').toHaveText('Welcome');
// 失败时会显示:
// Error: 标题应该显示欢迎信息
// Expected: Welcome
// Received: Hello
});
软断言 #
Playwright 默认使用硬断言,失败后立即停止。可以使用软断言继续执行:
typescript
test('软断言', async ({ page }) => {
// 收集所有失败
await expect.soft(page.locator('.title')).toHaveText('Welcome');
await expect.soft(page.locator('.subtitle')).toHaveText('Hello');
await expect.soft(page.locator('.description')).toHaveText('Description');
// 所有断言都会执行,最后汇总失败
});
否定断言 #
typescript
test('否定断言', async ({ page }) => {
// 使用 not
await expect(page.locator('.element')).not.toBeVisible();
await expect(page.locator('.element')).not.toHaveText('Hello');
await expect(page.locator('.element')).not.toBeEnabled();
// 特定否定方法
await expect(page.locator('.element')).toBeHidden(); // 等同于 not.toBeVisible()
await expect(page.locator('.element')).toBeDisabled(); // 等同于 not.toBeEnabled()
});
异步断言 #
typescript
test('异步断言', async ({ page }) => {
// 等待元素出现并断言
await expect(async () => {
const count = await page.locator('.item').count();
expect(count).toBeGreaterThan(0);
}).toPass();
// 自定义选项
await expect(async () => {
const text = await page.locator('.status').textContent();
expect(text).toContain('Complete');
}).toPass({
timeout: 10000,
intervals: [100, 500, 1000], // 重试间隔
});
});
断言最佳实践 #
1. 使用语义化断言 #
typescript
// ✅ 推荐
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('checkbox')).toBeChecked();
// ❌ 不推荐
await expect(await page.getByRole('button').isEnabled()).toBe(true);
await expect(await page.getByRole('checkbox').isChecked()).toBe(true);
2. 使用自动重试断言 #
typescript
// ✅ 推荐 - 自动重试
await expect(page.locator('.result')).toHaveText('Success');
// ❌ 不推荐 - 手动等待
await page.waitForSelector('.result');
const text = await page.locator('.result').textContent();
expect(text).toBe('Success');
3. 添加有意义的消息 #
typescript
// ✅ 推荐
await expect(page.locator('.error'), '登录失败时应显示错误消息').toBeVisible();
// ❌ 不推荐
await expect(page.locator('.error')).toBeVisible();
4. 合理使用软断言 #
typescript
// ✅ 推荐 - 需要检查多个独立条件时
test('页面完整性检查', async ({ page }) => {
await expect.soft(page.locator('.header')).toBeVisible();
await expect.soft(page.locator('.footer')).toBeVisible();
await expect.soft(page.locator('.sidebar')).toBeVisible();
});
// ❌ 不推荐 - 后续依赖前面的结果
test('登录流程', async ({ page }) => {
await expect.soft(page.locator('.login-form')).toBeVisible();
// 如果上面失败,下面的操作可能无意义
await page.getByLabel('Email').fill('test@example.com');
});
断言速查表 #
| 断言方法 | 描述 |
|---|---|
toBeVisible() |
元素可见 |
toBeHidden() |
元素隐藏 |
toBeEnabled() |
元素已启用 |
toBeDisabled() |
元素已禁用 |
toBeEditable() |
元素可编辑 |
toBeChecked() |
元素已选中 |
toBeFocused() |
元素已聚焦 |
toBeEmpty() |
元素为空 |
toHaveText() |
文本匹配 |
toContainText() |
包含文本 |
toHaveValue() |
值匹配 |
toHaveAttribute() |
属性匹配 |
toHaveClass() |
类名匹配 |
toHaveCSS() |
CSS 属性匹配 |
toHaveCount() |
数量匹配 |
toHaveURL() |
URL 匹配 |
toHaveTitle() |
标题匹配 |
toHaveScreenshot() |
快照匹配 |
下一步 #
现在你已经掌握了 Playwright 断言验证的方法,接下来学习 异步处理 了解如何处理异步场景!
最后更新:2026-03-28