Playwright 第12章:断言与自动等待
断言是测试验证的核心。Playwright 的断言库内置了强大的自动等待机制,使测试更加稳定可靠。本章系统介绍 Playwright 的各种断言方法、自动等待原理以及超时配置策略。
自动等待机制
自动等待原理
Playwright 的断言方法内置了自动等待和重试机制。当断言条件不满足时,Playwright 会持续重试直到超时:
import { test, expect } from '@playwright/test';
// 这个断言会持续重试直到元素可见或超时
await expect(page.locator('.dynamic-content')).toBeVisible();
// 相当于手动实现(但更简洁)
// await page.waitForSelector('.dynamic-content', { state: 'visible' });
自动等待的优势:
| 特性 | 说明 |
|---|---|
| 自动重试 | 默认每 100ms 检查一次,直到条件满足 |
| 智能等待 | 自动等待元素附加、可见、稳定、启用 |
| 减少 flakiness | 消除手动 sleep 导致的测试不稳定 |
| 清晰错误 | 超时时提供详细的错误信息 |
断言超时配置
// 全局配置(playwright.config.ts)
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
timeout: 10000, // 默认 5000ms
},
});
// 单条断言超时
await expect(page.locator('.slow-element')).toBeVisible({
timeout: 15000,
});
// 测试级别超时
test('慢速测试', { timeout: 60000 }, async ({ page }) => {
await expect(page.locator('.content')).toBeVisible({ timeout: 30000 });
});
常见断言方法
可见性与状态断言
const locator = page.locator('.submit-btn');
// 元素可见
await expect(locator).toBeVisible();
// 元素隐藏(不存在于 DOM 或不可见)
await expect(locator).toBeHidden();
// 元素已附加到 DOM
await expect(locator).toBeAttached();
// 元素已从 DOM 移除
await expect(locator).not.toBeAttached();
// 元素可用(非 disabled)
await expect(locator).toBeEnabled();
// 元素禁用
await expect(locator).toBeDisabled();
// 元素可编辑
await expect(locator).toBeEditable();
// 元素为空
await expect(locator).toBeEmpty();
// 复选框已选中
await expect(locator).toBeChecked();
// 复选框未选中
await expect(locator).not.toBeChecked();
// 元素聚焦
await expect(locator).toBeFocused();
// 元素在视口中可见
await expect(locator).toBeInViewport();
文本内容断言
const locator = page.locator('.welcome-message');
// 精确匹配文本
await expect(locator).toHaveText('欢迎回来,管理员');
// 包含文本
await expect(locator).toContainText('欢迎');
// 正则匹配
await expect(locator).toHaveText(/欢迎.*管理员/);
await expect(locator).toContainText(/欢迎|Hello/);
// 忽略大小写
await expect(locator).toHaveText('欢迎回来', { ignoreCase: true });
// 匹配多个元素的文本
const items = page.locator('.list-item');
await expect(items).toHaveText(['第一项', '第二项', '第三项']);
// 使用正则匹配多个
await expect(items).toHaveText([/第一/, /第二/, /第三/]);
属性与值断言
const locator = page.locator('#username');
// 验证属性
await expect(locator).toHaveAttribute('type', 'text');
await expect(locator).toHaveAttribute('placeholder', /请输入/);
// 验证多个属性
await expect(locator).toHaveAttribute({
type: 'text',
name: 'username',
required: '',
});
// 验证输入值
await expect(locator).toHaveValue('admin');
// 验证部分值
await expect(locator).toHaveValue(/admin|test/);
// 验证 CSS 属性
await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)');
await expect(locator).toHaveCSS('display', 'none');
// 验证类名
await expect(locator).toHaveClass(/active|highlight/);
数量与存在断言
const items = page.locator('.product-card');
// 精确数量
await expect(items).toHaveCount(24);
// 数量范围(使用 not)
await expect(items).not.toHaveCount(0);
// 验证列表为空
await expect(items).toHaveCount(0);
// 验证元素存在
await expect(items.first()).toBeAttached();
// 验证特定索引的元素
await expect(items.nth(2)).toContainText('特价商品');
URL 和页面断言
// 页面 URL
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveURL(/dashboard|home/);
await expect(page).toHaveURL('**/dashboard');
// 页面标题
await expect(page).toHaveTitle('管理后台');
await expect(page).toHaveTitle(/后台|管理/);
// 页面截图对比
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixelRatio: 0.01,
threshold: 0.2,
});
软断言
使用软断言
软断言(Soft Assert)允许测试在某个断言失败后继续执行,而不是立即终止:
import { test, expect } from '@playwright/test';
test('软断言示例', async ({ page }) => {
await page.goto('/profile');
// 使用软断言(失败不终止测试)
await expect.soft(page.locator('.avatar')).toBeVisible();
await expect.soft(page.locator('.username')).toHaveText('admin');
await expect.soft(page.locator('.email')).toHaveValue('[email protected]');
// 所有软断言执行完毕后检查是否有失败
// 如果有失败,测试会在最后终止
});
软断言的应用场景
test('表单验证 - 多字段检查', async ({ page }) => {
await page.goto('/register');
await page.click('.submit-btn');
// 检查所有验证错误消息
await expect.soft(page.locator('.error-name')).toBeVisible();
await expect.soft(page.locator('.error-email')).toBeVisible();
await expect.soft(page.locator('.error-password')).toBeVisible();
await expect.soft(page.locator('.error-terms')).toBeVisible();
// 验证错误数量
await expect(page.locator('.error-message')).toHaveCount(4);
// 如果这里执行到,说明所有软断言都通过了
});
test('批量数据验证', async ({ page }) => {
await page.goto('/data-table');
const rows = page.locator('table tr');
const rowCount = await rows.count();
// 验证每一行数据
for (let i = 1; i < rowCount; i++) {
const row = rows.nth(i);
await expect.soft(row.locator('.id')).toMatch(/^\d+$/);
await expect.soft(row.locator('.name')).not.toBeEmpty();
await expect.soft(row.locator('.status')).toContainText(/active|inactive/);
}
});
否定断言
const locator = page.locator('.error-message');
// 元素不可见
await expect(locator).not.toBeVisible();
// 元素不包含文本
await expect(locator).not.toContainText('错误');
// 元素没有特定属性
await expect(locator).not.toHaveAttribute('disabled', '');
// 数量不匹配
await expect(page.locator('.item')).not.toHaveCount(0);
// URL 不包含
await expect(page).not.toHaveURL('/error');
// 组合使用
await expect(locator).not.toBeVisible({ timeout: 2000 });
自定义等待条件
使用 expect.poll()
test('自定义轮询断言', async ({ page }) => {
await page.goto('/dashboard');
// 轮询直到条件满足
await expect.poll(async () => {
const count = await page.locator('.notification').count();
return count;
}, {
timeout: 10000,
message: '等待通知出现',
intervals: [1000, 2000, 3000],
}).toBeGreaterThan(0);
// 轮询 API 响应
await expect.poll(async () => {
const response = await page.request.get('/api/status');
const data = await response.json();
return data.status;
}, {
timeout: 30000,
}).toBe('completed');
});
使用 expect.toPass()
test('复杂条件断言', async ({ page }) => {
await page.goto('/analytics');
// 反复执行直到通过或超时
await expect(async () => {
await page.click('.refresh-btn');
await page.waitForTimeout(1000);
const chartData = await page.evaluate(() => {
return window.__chartData__;
});
expect(chartData).toBeDefined();
expect(chartData.points.length).toBeGreaterThan(0);
expect(chartData.latestValue).toBeGreaterThan(100);
}).toPass({
timeout: 30000,
intervals: [2000, 3000, 5000],
});
});
断言最佳实践
优先使用定位器断言
// 推荐:使用定位器断言(自动等待)
await expect(page.locator('.success')).toBeVisible();
// 避免:手动等待后判断
await page.waitForTimeout(2000);
const isVisible = await page.locator('.success').isVisible();
expect(isVisible).toBe(true);
合理设置超时
// 快速操作使用短超时
await expect(page.locator('.fast-element')).toBeVisible({ timeout: 2000 });
// 慢速操作使用长超时
await expect(page.locator('.lazy-content')).toBeVisible({ timeout: 30000 });
// 全局超时设置
// playwright.config.ts
expect: {
timeout: 10000,
},
错误信息优化
// 添加自定义错误消息
await expect(page.locator('.welcome'), '登录后应显示欢迎信息').toBeVisible();
// 在断言前截图以便调试
test('调试失败的断言', async ({ page }) => {
await page.goto('/login');
await page.fill('#username', 'admin');
await page.fill('#password', 'wrong');
await page.click('button[type="submit"]');
try {
await expect(page.locator('.welcome')).toBeVisible({ timeout: 3000 });
} catch (error) {
await page.screenshot({ path: 'debug-screenshot.png', fullPage: true });
throw error;
}
});
总结
Playwright 的断言系统最强大的特性是自动等待机制,它消除了传统测试中大量令人头疼的手动等待和重试逻辑。建议优先使用 expect(locator).toBeVisible() 等带有自动等待的断言方法,而非 isVisible() 等被动检查。软断言适用于需要收集多个验证结果的场景。合理配置断言超时时间,在稳定性和执行效率之间找到平衡点。