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