Playwright自动化测试实战指南-中级部分
版本说明:本文基于 Playwright v1.32.0 版本,介绍了自动化测试的进阶应用场景和技巧。由于Playwright更新较快,部分API可能在新版本中有所变化,建议同时参考官方文档获取最新信息。
引言
随着Web应用日益复杂,自动化测试变得愈发重要。Playwright作为一款现代化的自动化测试工具,提供了强大的跨浏览器测试能力。本文适合已经掌握Playwright基础知识,希望进一步提升测试技能的开发者和测试工程师阅读。我们将深入探讨Playwright的进阶应用,帮助你构建更加健壮、高效的测试方案。
目录
中级应用
1. 截图与视频录制
Playwright 提供了强大的截图和视频录制功能,便于调试和记录测试过程。
截图功能
// 截取整个页面
await page.screenshot({ path: 'page.png' });
// 截取特定元素
const element = await page.locator('.header');
await element.screenshot({ path: 'header.png' });
// 全页面截图(包括滚动部分)
await page.screenshot({
path: 'fullpage.png',
fullPage: true
});
// 剪裁截图
await page.screenshot({
path: 'clipped.png',
clip: {
x: 100,
y: 100,
width: 500,
height: 300
}
});
// 忽略HTTPS错误的截图
await page.screenshot({
path: 'screenshot.png',
ignoreHTTPSErrors: true
});
// 截图时隐藏某些元素
await page.evaluate(() => {
const banner = document.querySelector('.cookie-banner');
if (banner) banner.style.display = 'none';
});
await page.screenshot({ path: 'no-banner.png' });
视频录制
Playwright 可以录制测试执行的视频,非常适合复杂用例的调试和问题复现。
// 在测试配置文件中启用视频录制
// playwright.config.js
module.exports = {
use: {
video: {
mode: 'on', // 始终录制
// 其他选项:'off', 'on-first-retry', 'retain-on-failure', 'on-first-retry'
size: { width: 1280, height: 720 } // 自定义尺寸
}
}
};
单个测试中启用录制:
// 单个测试中的配置
test.use({
video: 'on'
});
test('录制登录过程', async ({ page }) => {
await page.goto('https://example.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('https://example.com/dashboard');
});
高级截图和录制技巧
截图比较
// 截图比较和像素匹配
const screenshot = await page.screenshot();
// 使用像素匹配进行对比
expect(screenshot).toMatchSnapshot('reference.png', {
threshold: 0.2, // 允许20%的像素差异
});
有条件的视频录制
// 仅在特定条件下录制视频
const context = await browser.newContext({
recordVideo: process.env.RECORD_VIDEO ? {
dir: 'videos/',
size: { width: 1024, height: 768 }
} : undefined
});
截图最佳实践
- 用途明确的命名:使用描述性文件名(如
login-error-state.png
) - 保持一致的视口大小:在截图前设置统一的视口大小
- 在CI系统中自动执行截图:结合持续集成自动生成和存储截图
- 处理动态内容:在截图前隐藏或模拟时间戳等动态元素
2. 测试框架集成
Playwright 可以与多种测试框架无缝集成,提升测试流程效率。
与 Jest 集成
// 安装依赖:
// npm install -D jest playwright @playwright/test jest-playwright-preset
// jest.config.js
module.exports = {
preset: 'jest-playwright-preset',
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
testEnvironmentOptions: {
'jest-playwright': {
browsers: ['chromium', 'firefox', 'webkit'],
launchOptions: {
headless: true
}
}
}
};
// login.test.js
describe('登录功能', () => {
beforeAll(async () => {
await page.goto('https://example.com/login');
});
test('成功登录', async () => {
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('https://example.com/dashboard');
const welcomeText = await page.textContent('.welcome-message');
expect(welcomeText).toContain('Welcome, Test User');
});
});
与 Mocha 集成
// 安装依赖:
// npm install -D mocha @playwright/test
// test/login.spec.js
const { chromium } = require('playwright');
const { expect } = require('chai');
describe('登录功能测试', () => {
let browser;
let page;
before(async () => {
browser = await chromium.launch();
});
beforeEach(async () => {
page = await browser.newPage();
await page.goto('https://example.com/login');
});
afterEach(async () => {
await page.close();
});
after(async () => {
await browser.close();
});
it('应该成功登录', async () => {
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
expect(page.url()).to.include('/dashboard');
const welcomeText = await page.textContent('.welcome-message');
expect(welcomeText).to.contain('Welcome, Test User');
});
});
与 Cucumber 集成
// 安装依赖:
// npm install -D @cucumber/cucumber playwright
// features/login.feature
Feature: 用户登录
用户应该能够登录到系统
Scenario: 成功登录
Given 用户在登录页面
When 用户输入有效的用户名和密码
And 用户点击登录按钮
Then 用户应该被重定向到仪表板
And 用户应该看到欢迎消息
// step-definitions/login.steps.js
const { Given, When, Then } = require('@cucumber/cucumber');
const { chromium } = require('playwright');
const { expect } = require('chai');
let browser;
let page;
Given('用户在登录页面', async function() {
browser = await chromium.launch();
page = await browser.newPage();
await page.goto('https://example.com/login');
});
When('用户输入有效的用户名和密码', async function() {
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
});
When('用户点击登录按钮', async function() {
await page.click('button[type="submit"]');
});
Then('用户应该被重定向到仪表板', async function() {
await page.waitForURL('**/dashboard');
expect(page.url()).to.include('/dashboard');
});
Then('用户应该看到欢迎消息', async function() {
const welcomeText = await page.textContent('.welcome-message');
expect(welcomeText).to.contain('Welcome, Test User');
await browser.close();
});
测试框架集成最佳实践
- 选择适合团队的框架:根据团队熟悉度和项目需求选择
- 保持简单的测试结构:使用页面对象模式组织测试代码
- 复用测试步骤:提取常见操作到辅助函数中
- 配置共享环境:使用配置文件共享浏览器设置
- 统一断言风格:在项目中保持一致的断言方式
3. 移动设备模拟
Playwright 提供了完善的移动设备模拟功能,帮助测试响应式网站行为。
设备模拟基础
const { devices } = require('@playwright/test');
// 使用预定义设备
test.use({ ...devices['iPhone 13'] });
test('在iPhone上访问网站', async ({ page }) => {
await page.goto('https://example.com');
// 验证移动版UI元素
const mobileMenu = page.locator('.mobile-menu-button');
await expect(mobileMenu).toBeVisible();
// 测试汉堡菜单点击
await mobileMenu.click();
await expect(page.locator('.mobile-menu-dropdown')).toBeVisible();
});
自定义设备配置
// 自定义设备配置
test.use({
viewport: { width: 375, height: 812 },
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1'
});
test('测试自定义移动设备', async ({ page }) => {
await page.goto('https://example.com');
// 检查响应式行为
});
模拟触摸事件
// 模拟触摸事件
test('测试滑动手势', async ({ page }) => {
await page.goto('https://example.com/photos');
// 获取轮播图元素
const carousel = page.locator('.image-carousel');
const boundingBox = await carousel.boundingBox();
// 执行从右向左的滑动手势
await page.touchscreen.tap(
boundingBox.x + boundingBox.width - 50,
boundingBox.y + boundingBox.height / 2
);
await page.mouse.down();
await page.mouse.move(
boundingBox.x + 50,
boundingBox.y + boundingBox.height / 2
);
await page.mouse.up();
// 验证轮播图已切换到下一张
await expect(page.locator('.carousel-indicator.active')).toHaveAttribute('data-index', '1');
});
测试横竖屏切换
// 测试横竖屏切换
test('测试横竖屏响应', async ({ browser }) => {
// iPhone 13 竖屏
const verticalContext = await browser.newContext({
...devices['iPhone 13'],
});
const verticalPage = await verticalContext.newPage();
await verticalPage.goto('https://example.com');
// 检查竖屏布局
const verticalMenuWidth = await verticalPage.locator('nav').evaluate(el => el.offsetWidth);
// iPhone 13 横屏
const landscapeContext = await browser.newContext({
...devices['iPhone 13 landscape'],
});
const landscapePage = await landscapeContext.newPage();
await landscapePage.goto('https://example.com');
// 检查横屏布局
const landscapeMenuWidth = await landscapePage.locator('nav').evaluate(el => el.offsetWidth);
// 验证响应式设计按预期工作
expect(landscapeMenuWidth).toBeGreaterThan(verticalMenuWidth);
await verticalContext.close();
await landscapeContext.close();
});
移动设备模拟最佳实践
- 测试关键设备:至少测试iOS和Android的主流屏幕尺寸
- 检查触摸操作区域:确保点击目标足够大(至少48×48像素)
- 测试手势操作:包括滑动、捏合等常见手势
- 验证媒体查询:确认响应式断点正确触发
- 测试性能:在移动设备配置下检查页面加载性能
4. 网络请求拦截与模拟
Playwright 提供了强大的网络请求拦截和模拟功能,可以测试各种网络场景。
注意:以下API示例基于Playwright v1.32.0,请参考最新文档了解可能的变更。
基本请求拦截
// 拦截请求并修改响应
test('拦截和修改API响应', async ({ page }) => {
// 拦截API请求
await page.route('**/api/users', route => {
// 返回自定义数据
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: '测试用户1' },
{ id: 2, name: '测试用户2' }
])
});
});
await page.goto('https://example.com/users');
// 验证页面显示了模拟数据
const userElements = page.locator('.user-item');
await expect(userElements).toHaveCount(2);
await expect(page.locator('.user-item:first-child')).toContainText('测试用户1');
});
模拟网络错误
// 模拟网络错误
test('处理API错误', async ({ page }) => {
// 模拟API服务器错误
await page.route('**/api/products', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: '服务器内部错误' })
});
});
await page.goto('https://example.com/products');
// 验证错误消息显示
await expect(page.locator('.error-message')).toBeVisible();
await expect(page.locator('.error-message')).toContainText('无法加载产品');
});
网络请求验证
// 验证发送的请求参数
test('验证搜索请求参数', async ({ page }) => {
let requestData = null;
// 拦截并检查请求
await page.route('**/api/search', route => {
requestData = route.request().postDataJSON();
route.continue(); // 允许请求继续
});
await page.goto('https://example.com/search');
await page.fill('#search-input', '测试关键词');
await page.click('#search-button');
// 验证搜索请求参数
expect(requestData).toEqual({
query: '测试关键词',
page: 1
});
});
模拟慢速网络
// 模拟慢速网络
test('在慢速连接下测试加载状态', async ({ page }) => {
// 模拟3G网络
await page.route('**/*', route => {
// 为所有请求添加延迟
setTimeout(() => {
route.continue();
}, 300); // 添加300ms延迟
});
await page.goto('https://example.com/dashboard');
// 验证加载指示器显示
await expect(page.locator('.loading-spinner')).toBeVisible();
// 等待内容最终加载
await expect(page.locator('.dashboard-content')).toBeVisible();
await expect(page.locator('.loading-spinner')).toBeHidden();
});
模拟离线状态
// 模拟离线状态
test('测试离线模式', async ({ browser }) => {
// 创建离线上下文
const context = await browser.newContext({
offline: true
});
const page = await context.newPage();
// 访问页面
await page.goto('https://example.com').catch(e => {
// 预期会有网络错误
expect(e.message).toContain('net::ERR_INTERNET_DISCONNECTED');
});
// 如果有离线页面,测试其是否正常显示
if (await page.locator('.offline-message').isVisible()) {
await expect(page.locator('.offline-message')).toContainText('您当前处于离线状态');
}
await context.close();
});
高级请求处理
// 条件请求处理
test('根据请求参数条件处理', async ({ page }) => {
// 处理不同参数的请求
await page.route('**/api/products**', route => {
const url = route.request().url();
const params = new URL(url).searchParams;
const category = params.get('category');
if (category === 'electronics') {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: '智能手机', price: 1999 },
{ id: 2, name: '笔记本电脑', price: 5999 }
])
});
} else if (category === 'books') {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 3, name: '编程指南', price: 59 },
{ id: 4, name: '科幻小说', price: 45 }
])
});
} else {
// 默认响应
route.continue();
}
});
// 测试电子产品类别
await page.goto('https://example.com/products?category=electronics');
await expect(page.locator('.product-item')).toHaveCount(2);
await expect(page.locator('.product-item:first-child')).toContainText('智能手机');
// 测试图书类别
await page.goto('https://example.com/products?category=books');
await expect(page.locator('.product-item')).toHaveCount(2);
await expect(page.locator('.product-item:first-child')).toContainText('编程指南');
});
网络请求处理最佳实践
- 选择性拦截:只拦截需要模拟的请求,允许其他请求正常进行
- 保持数据一致性:确保模拟的响应格式与真实API一致
- 测试各种网络状况:包括慢速、离线和间歇性连接
- 验证请求参数:确保应用程序发送正确的请求数据
- 模拟常见错误状态:测试应用如何处理401、404、500等错误
5. 并行测试执行
Playwright 支持并行执行测试,大幅提高测试效率。
基本并行配置
在 playwright.config.js
中配置并行执行:
// playwright.config.js
module.exports = {
// 并发执行数,设为"1"禁用并发
workers: 3,
// 在多个浏览器上并行运行
projects: [
{
name: 'Chromium',
use: { browserName: 'chromium' }
},
{
name: 'Firefox',
use: { browserName: 'firefox' }
},
{
name: 'WebKit',
use: { browserName: 'webkit' }
}
]
};
按测试特性分组
// playwright.config.js
module.exports = {
workers: 3,
// 按测试特性分组
projects: [
{
name: '桌面版 Chrome',
use: {
browserName: 'chromium',
viewport: { width: 1280, height: 720 }
},
testMatch: /.*desktop.spec.js/
},
{
name: '移动版 Chrome',
use: {
browserName: 'chromium',
...devices['Pixel 5']
},
testMatch: /.*mobile.spec.js/
},
{
name: '电子商务测试',
use: { browserName: 'chromium' },
testDir: './tests/e-commerce'
}
]
};
数据依赖处理
处理依赖共享数据的测试:
// 使用全局设置创建测试数据
// global-setup.js
const { chromium } = require('@playwright/test');
const fs = require('fs');
module.exports = async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// 登录并获取认证令牌
await page.goto('https://example.com/login');
await page.fill('#username', 'admin');
await page.fill('#password', 'admin123');
await page.click('button[type="submit"]');
// 等待登录完成
await page.waitForURL('**/dashboard');
// 获取认证状态(例如,保存 cookies)
const authData = await context.storageState();
// 保存认证状态到文件
fs.writeFileSync('auth-state.json', JSON.stringify(authData));
await browser.close();
};
// playwright.config.js
module.exports = {
globalSetup: './global-setup.js',
// 测试配置
projects: [
{
name: '已验证用户',
use: {
// 使用预先保存的认证状态
storageState: 'auth-state.json'
}
}
]
};
测试隔离与共享状态
// 每个测试文件独立执行设置
// tests/admin-panel.spec.js
const { test, expect } = require('@playwright/test');
// 文件级别的设置,所有测试共享
test.beforeAll(async ({ browser }) => {
// 执行测试所需的前置准备
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com/setup');
await page.fill('#entity-name', 'Test Entity ' + Date.now());
await page.click('#create-entity');
// 保存全局状态供测试使用
test.setTimeout(60000); // 增加超时时间
test.info().annotations.push({
type: 'entity',
description: await page.locator('#entity-id').textContent()
});
await context.close();
});
// 清理测试数据
test.afterAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
// 删除测试数据
await page.goto('https://example.com/cleanup');
const entityId = test.info().annotations.find(a => a.type === 'entity').description;
await page.fill('#entity-id', entityId);
await page.click('#delete-entity');
await context.close();
});
并行测试最佳实践
- 合理设置工作进程数:通常为CPU核心数的1-2倍
- 保持测试独立性:避免测试间的依赖关系
- 使用唯一标识符:避免测试数据冲突(使用时间戳或UUID)
- 监控资源使用:防止过多的并行测试导致资源耗尽
- 使用全局设置:预先创建需要的测试数据
- 考虑测试顺序:对于不能并行的测试,使用串行执行
高级并行策略
// playwright.config.js
const os = require('os');
module.exports = {
// 根据系统资源动态调整并发数
workers: process.env.CI ? 4 : Math.max(os.cpus().length - 1, 1),
// 分阶段执行测试
projects: [
// 先执行关键路径测试
{
name: '关键路径',
testMatch: /.*critical.spec.js/,
retries: 2 // 关键测试失败时重试
},
// 再执行功能测试
{
name: '功能测试',
testMatch: /.*feature.spec.js/,
dependencies: ['关键路径'] // 等待关键路径测试完成
},
// 最后执行其他测试
{
name: '边缘情况',
testMatch: /.*edge-case.spec.js/,
dependencies: ['功能测试']
}
],
// 全局超时设置
timeout: 30000,
// 报告配置
reporter: [
['list'],
['html', { open: 'never' }],
['junit', { outputFile: 'test-results.xml' }]
]
};
高级应用
1. 身份验证管理
有效管理身份验证状态对于测试需要登录的应用程序至关重要。Playwright提供了多种方式来处理身份验证。
会话存储与重用
// 保存身份验证状态,避免重复登录
const { chromium } = require('@playwright/test');
async function authenticateAndSaveState() {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// 执行登录
await page.goto('https://example.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
// 等待登录成功
await page.waitForURL('**/dashboard');
// 保存认证状态到文件
await context.storageState({ path: 'auth.json' });
await browser.close();
}
// 在测试中使用保存的认证状态
test.use({ storageState: 'auth.json' });
test('已登录状态测试', async ({ page }) => {
await page.goto('https://example.com/profile');
// 无需登录,直接进行已认证状态的测试
await expect(page.locator('.user-name')).toContainText('testuser');
});
多角色测试
// 不同用户角色的认证管理
// auth-setup.js
const { chromium } = require('@playwright/test');
const fs = require('fs');
async function setupAuth() {
// 创建角色目录
if (!fs.existsSync('./auth')) {
fs.mkdirSync('./auth');
}
// 管理员登录
await loginAs('admin', 'admin123', './auth/admin.json');
// 普通用户登录
await loginAs('user', 'user123', './auth/user.json');
// 访客用户(只做最低权限验证)
await loginAs('guest', 'guest123', './auth/guest.json');
}
async function loginAs(username, password, savePathFile) {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com/login');
await page.fill('#username', username);
await page.fill('#password', password);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
await context.storageState({ path: savePathFile });
await browser.close();
}
module.exports = setupAuth;
// playwright.config.js中配置
// projects: [
// {
// name: '管理员测试',
// use: { storageState: './auth/admin.json' },
// testMatch: /.*admin.spec.js/
// },
// {
// name: '用户测试',
// use: { storageState: './auth/user.json' },
// testMatch: /.*user.spec.js/
// },
// {
// name: '访客测试',
// use: { storageState: './auth/guest.json' },
// testMatch: /.*guest.spec.js/
// }
// ]
处理双因素认证
// 处理双因素认证
test('处理双因素认证', async ({ page }) => {
// 常规登录
await page.goto('https://example.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
// 等待2FA页面加载
await page.waitForURL('**/2fa');
// 模拟从身份验证器应用获取代码
// 实际测试中,可能需要集成第三方库生成OTP代码
const otpCode = generateOtpCode('JBSWY3DPEHPK3PXP'); // 示例密钥
await page.fill('#otp-code', otpCode);
await page.click('#verify-button');
// 验证登录成功
await page.waitForURL('**/dashboard');
await expect(page.locator('.welcome-message')).toBeVisible();
});
// 辅助函数:生成OTP代码(示例)
function generateOtpCode(secret) {
// 在实际测试中,使用像otplib这样的库
// const { authenticator } = require('otplib');
// return authenticator.generate(secret);
// 简化示例,返回固定代码
return '123456';
}
身份验证最佳实践
- 分离认证逻辑:将认证步骤与测试业务逻辑分开
- 重用认证状态:通过保存和加载认证状态,避免每次测试都登录
- 测试权限边界:确保针对不同用户角色进行测试
- 安全处理凭据:避免在代码中硬编码敏感信息,使用环境变量
- 模拟认证服务:在单元测试中考虑模拟认证服务
2. 高级事件处理
现代web应用包含许多复杂的事件和异步行为,Playwright提供了强大的工具来测试这些情况。
WebSocket监听与测试
// 测试WebSocket连接
test('WebSocket通信测试', async ({ page }) => {
// 创建WebSocket消息收集器
const wsMessages = [];
// 监听WebSocket通信
page.on('websocket', ws => {
console.log(`WebSocket连接已打开: ${ws.url()}`);
ws.on('framesent', event => {
wsMessages.push({type: 'sent', data: event.payload});
});
ws.on('framereceived', event => {
wsMessages.push({type: 'received', data: event.payload});
});
ws.on('close', () => {
console.log('WebSocket连接已关闭');
});
});
// 导航到使用WebSocket的页面
await page.goto('https://example.com/chat');
// 触发WebSocket通信
await page.fill('#message-input', '测试消息');
await page.click('#send-button');
// 等待接收响应
await page.waitForTimeout(1000);
// 验证WebSocket通信
const sentMessages = wsMessages.filter(m => m.type === 'sent');
expect(sentMessages.some(m => m.data.includes('测试消息'))).toBeTruthy();
// 验证UI响应
await expect(page.locator('.message-list .message').last()).toContainText('测试消息');
});
监听网络事件
// 高级网络事件监听
test('跟踪所有网络请求与响应', async ({ page }) => {
// 存储请求和响应信息
const requests = [];
const responses = [];
const requestsFailed = [];
const requestsFinished = [];
// 监听网络事件
page.on('request', request => {
requests.push({
url: request.url(),
method: request.method(),
headers: request.headers(),
postData: request.postData()
});
});
page.on('response', response => {
responses.push({
url: response.url(),
status: response.status(),
headers: response.headers()
});
});
page.on('requestfailed', request => {
requestsFailed.push({
url: request.url(),
failure: request.failure()
});
});
page.on('requestfinished', request => {
requestsFinished.push({
url: request.url(),
size: request.sizes()
});
});
// 访问页面并执行操作
await page.goto('https://example.com/dashboard');
await page.click('#refresh-data');
// 等待所有网络活动完成
await page.waitForLoadState('networkidle');
// 分析网络活动
console.log(`总请求数: ${requests.length}`);
console.log(`成功响应数: ${responses.filter(r => r.status < 400).length}`);
console.log(`失败请求数: ${requestsFailed.length}`);
// 验证关键API调用
expect(requests.some(r => r.url.includes('/api/dashboard-data'))).toBeTruthy();
// 检查是否有错误请求
const errResponses = responses.filter(r => r.status >= 400);
expect(errResponses).toHaveLength(0);
});
Service Worker测试
// Service Worker测试
test('测试Service Worker功能', async ({ page }) => {
// 监听Service Worker
page.on('serviceworker', worker => {
console.log('Service Worker已启动:', worker.url());
});
// 导航到使用Service Worker的页面
await page.goto('https://example.com/pwa');
// 确认Service Worker已注册
const swRegistered = await page.evaluate(() => {
return navigator.serviceWorker.controller !== null;
});
expect(swRegistered).toBeTruthy();
// 测试离线功能
// 1. 先缓存必要资源
await page.click('#cache-resources');
// 2. 启用离线模式
await page.context().setOffline(true);
// 3. 刷新页面测试离线访问
await page.reload();
// 4. 验证离线内容可访问
await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('.offline-ready')).toBeVisible();
// 恢复在线状态
await page.context().setOffline(false);
});
文件下载与上传测试
// 文件下载测试
test('测试文件下载', async ({ page }) => {
// 设置下载路径
const downloadPath = path.join(__dirname, 'downloads');
if (!fs.existsSync(downloadPath)) {
fs.mkdirSync(downloadPath);
}
// 配置下载行为
page.context().on('download', async download => {
console.log('开始下载:', download.suggestedFilename());
// 等待下载完成,并获取保存路径
const downloadedPath = await download.path();
// 将文件保存到指定目录
const filePath = path.join(downloadPath, download.suggestedFilename());
await download.saveAs(filePath);
console.log('文件已保存到:', filePath);
});
// 访问下载页面
await page.goto('https://example.com/downloads');
// 触发文件下载
await page.click('#download-csv');
// 等待下载完成
// 注意:需要一种方式来确认下载完成
await page.waitForTimeout(3000); // 简化示例,实际应使用更可靠的方法
// 验证文件存在
const expectedFile = path.join(downloadPath, 'data.csv');
expect(fs.existsSync(expectedFile)).toBeTruthy();
// 验证文件内容
const fileContent = fs.readFileSync(expectedFile, 'utf8');
expect(fileContent).toContain('id,name,value');
});
// 文件上传测试
test('测试文件上传', async ({ page }) => {
// 创建测试文件
const testFilePath = path.join(__dirname, 'test-upload.txt');
fs.writeFileSync(testFilePath, 'This is a test file for upload');
// 访问上传页面
await page.goto('https://example.com/upload');
// 上传文件
await page.setInputFiles('input[type="file"]', testFilePath);
// 提交表单
await page.click('#upload-button');
// 等待上传完成
await expect(page.locator('.upload-success')).toBeVisible();
// 验证上传后的文件名显示
const displayedName = await page.textContent('.uploaded-file-name');
expect(displayedName).toBe('test-upload.txt');
// 清理测试文件
fs.unlinkSync(testFilePath);
});
事件处理最佳实践
- 超时管理:为异步操作设置合理的超时时间
- 错误处理:捕获并记录事件处理过程中的错误
- 稳定性考虑:处理间歇性网络问题和重试逻辑
- 模拟条件:测试各种网络状况和边缘情况
- 隔离测试:确保测试间不会相互干扰事件监听
3. 可访问性测试
确保网站对所有用户可访问是现代网页开发的重要部分。Playwright可以集成可访问性测试工具。
基本可访问性审查
// 安装依赖:
// npm install -D axe-playwright
// 使用Axe进行可访问性测试
const { AxeBuilder } = require('@axe-core/playwright');
test('基本可访问性检查', async ({ page }) => {
await page.goto('https://example.com');
// 运行可访问性检查
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
// 断言没有违规
expect(accessibilityScanResults.violations).toHaveLength(0);
});
// 特定部分的可访问性检查
test('登录表单可访问性', async ({ page }) => {
await page.goto('https://example.com/login');
// 针对特定元素进行可访问性检查
const loginForm = await page.locator('#login-form');
const results = await new AxeBuilder({ page })
.include('#login-form')
.analyze();
// 输出违规详情用于调试
if (results.violations.length > 0) {
console.log(JSON.stringify(results.violations, null, 2));
}
expect(results.violations).toHaveLength(0);
});
自定义可访问性规则
// 自定义可访问性规则
test('使用自定义规则集进行检查', async ({ page }) => {
await page.goto('https://example.com/dashboard');
// 配置特定规则
const results = await new AxeBuilder({ page })
.withRules(['color-contrast', 'aria-roles', 'image-alt'])
.analyze();
expect(results.violations).toHaveLength(0);
});
// 排除特定元素
test('排除第三方内容的可访问性检查', async ({ page }) => {
await page.goto('https://example.com/with-third-party');
// 排除第三方小部件
const results = await new AxeBuilder({ page })
.exclude('.third-party-widget')
.exclude('iframe[src*="ads.example.com"]')
.analyze();
expect(results.violations).toHaveLength(0);
});
生成可访问性报告
// 生成详细的可访问性报告
test('生成HTML可访问性报告', async ({ page }) => {
await page.goto('https://example.com');
// 运行检查
const results = await new AxeBuilder({ page }).analyze();
// 保存结果为HTML报告
const reportHTML = `
<!DOCTYPE html>
<html>
<head>
<title>可访问性审查报告</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.violation { background: #ffebee; padding: 10px; margin-bottom: 10px; border-radius: 4px; }
.pass { background: #e8f5e9; padding: 10px; margin-bottom: 10px; border-radius: 4px; }
h2 { color: #d32f2f; }
h3 { margin-top: 0; }
pre { background: #f5f5f5; padding: 10px; overflow: auto; }
</style>
</head>
<body>
<h1>可访问性审查报告 - ${new Date().toLocaleString()}</h1>
<p>URL: ${page.url()}</p>
<h2>违规 (${results.violations.length})</h2>
${results.violations.map(v => `
<div class="violation">
<h3>${v.id}: ${v.help}</h3>
<p>影响: ${v.impact} | ${v.nodes.length} 个问题</p>
<p>${v.helpUrl}</p>
<pre>${JSON.stringify(v.nodes.map(n => n.html), null, 2)}</pre>
</div>
`).join('')}
<h2>通过 (${results.passes.length})</h2>
${results.passes.map(p => `
<div class="pass">
<h3>${p.id}: ${p.help}</h3>
<p>${p.nodes.length} 个元素通过</p>
</div>
`).join('')}
</body>
</html>
`;
// 保存报告
fs.writeFileSync('accessibility-report.html', reportHTML);
// 验证是否有严重违规
const severeViolations = results.violations.filter(v => v.impact === 'critical' || v.impact === 'serious');
expect(severeViolations).toHaveLength(0);
});
可访问性测试最佳实践
- 集成到CI/CD:将可访问性测试作为持续集成的一部分
- 设置基线:先建立基准,逐步解决问题
- 优先处理严重问题:先解决严重和关键级别的可访问性问题
- 测试键盘导航:确保网站可以仅使用键盘操作
- 考虑屏幕阅读器:测试与屏幕阅读器的兼容性
- 检查颜色对比度:确保文本与背景的对比度符合标准
- 检查ARIA属性:验证ARIA属性的正确使用
4. 性能测试与监控
Playwright 可以用于基本的性能测试和监控,帮助识别网站的性能瓶颈。
基本性能指标收集
// 基本性能指标收集
test('收集页面性能指标', async ({ page }) => {
// 导航到目标页面并收集性能数据
const navigationTimingJson = await page.evaluate(() => JSON.stringify(performance.timing));
const navigationTiming = JSON.parse(navigationTimingJson);
// 计算关键性能指标
const pageLoadTime = navigationTiming.loadEventEnd - navigationTiming.navigationStart;
const domContentLoadedTime = navigationTiming.domContentLoadedEventEnd - navigationTiming.navigationStart;
const firstPaintTime = await page.evaluate(() => {
const paintMetrics = performance.getEntriesByType('paint');
return paintMetrics.find(metric => metric.name === 'first-paint')?.startTime;
});
console.log(`页面加载时间: ${pageLoadTime}ms`);
console.log(`DOM内容加载时间: ${domContentLoadedTime}ms`);
console.log(`首次绘制时间: ${firstPaintTime}ms`);
// 性能断言(根据实际要求调整阈值)
expect(pageLoadTime).toBeLessThan(3000); // 页面加载时间小于3秒
expect(firstPaintTime).toBeLessThan(1000); // 首次绘制时间小于1秒
});
资源加载性能
// 分析资源加载性能
test('分析资源加载性能', async ({ page }) => {
// 创建性能观察器
const resourcesInfo = [];
// 监听所有资源请求
page.on('requestfinished', async request => {
const response = request.response();
if (!response) return;
try {
const responseBody = response.status() !== 304 ? await response.body() : '';
resourcesInfo.push({
url: request.url(),
resourceType: request.resourceType(),
status: response.status(),
size: responseBody.length,
duration: response.timing().responseEnd - response.timing().requestStart,
timing: response.timing()
});
} catch (error) {
console.error('无法获取响应体:', error);
}
});
// 导航到目标页面
await page.goto('https://example.com', { waitUntil: 'networkidle' });
// 汇总资源类型
const resourceTypeSummary = resourcesInfo.reduce((acc, resource) => {
const type = resource.resourceType;
if (!acc[type]) {
acc[type] = {
count: 0,
totalSize: 0,
totalDuration: 0
};
}
acc[type].count++;
acc[type].totalSize += resource.size;
acc[type].totalDuration += resource.duration;
return acc;
}, {});
// 输出资源加载摘要
console.log('资源加载摘要:');
for (const [type, stats] of Object.entries(resourceTypeSummary)) {
console.log(`类型: ${type}, 数量: ${stats.count}, 总大小: ${stats.totalSize / 1024}KB, 平均加载时间: ${stats.totalDuration / stats.count}ms`);
}
// 识别大型资源
const largeResources = resourcesInfo
.filter(r => r.size > 1024 * 500) // 大于500KB的资源
.sort((a, b) => b.size - a.size);
console.log('大型资源:');
largeResources.forEach(r => {
console.log(`URL: ${r.url}, 类型: ${r.resourceType}, 大小: ${r.size / 1024}KB`);
});
// 验证性能要求
const jsResources = resourcesInfo.filter(r => r.resourceType === 'script');
const totalJsSize = jsResources.reduce((sum, r) => sum + r.size, 0) / 1024;
expect(totalJsSize).toBeLessThan(1000); // JavaScript总大小小于1MB
});
使用Lighthouse集成
// 使用Lighthouse进行性能测试
// 需要安装: npm install -D lighthouse chrome-launcher
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const { test, expect } = require('@playwright/test');
const fs = require('fs');
const path = require('path');
test('使用Lighthouse进行性能分析', async () => {
// 启动Chrome
const chrome = await chromeLauncher.launch({
chromeFlags: ['--headless', '--disable-gpu', '--no-sandbox']
});
// 运行Lighthouse分析
const options = {
logLevel: 'info',
output: 'html',
port: chrome.port,
onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo']
};
const result = await lighthouse('https://example.com', options);
// 确保报告目录存在
const reportDir = path.join(__dirname, 'lighthouse-reports');
if (!fs.existsSync(reportDir)) {
fs.mkdirSync(reportDir);
}
// 保存HTML报告
const reportPath = path.join(reportDir, `lighthouse-report-${Date.now()}.html`);
fs.writeFileSync(reportPath, result.report);
// 打印性能分数
console.log(`性能得分: ${result.lhr.categories.performance.score * 100}/100`);
console.log(`可访问性得分: ${result.lhr.categories.accessibility.score * 100}/100`);
console.log(`最佳实践得分: ${result.lhr.categories['best-practices'].score * 100}/100`);
console.log(`SEO得分: ${result.lhr.categories.seo.score * 100}/100`);
// 性能断言
expect(result.lhr.categories.performance.score).toBeGreaterThanOrEqual(0.7); // 性能得分>=70
expect(result.lhr.categories.accessibility.score).toBeGreaterThanOrEqual(0.9); // 可访问性得分>=90
// 关闭Chrome
await chrome.kill();
});
跟踪用户交互时间
// 测量用户交互响应时间
test('测量页面交互响应性能', async ({ page }) => {
await page.goto('https://example.com/app');
// 等待页面完全加载
await page.waitForLoadState('networkidle');
// 性能测量:切换标签页响应时间
const tabs = page.locator('.tab');
const tabCount = await tabs.count();
const tabSwitchTimes = [];
for (let i = 0; i < tabCount; i++) {
// 记录切换开始时间
const startTime = Date.now();
// 点击标签
await tabs.nth(i).click();
// 等待内容加载
await page.locator(`.tab-content[data-tab="${i}"]`).waitFor();
// 计算响应时间
const responseTime = Date.now() - startTime;
tabSwitchTimes.push({ tab: i, time: responseTime });
console.log(`标签 ${i} 响应时间: ${responseTime}ms`);
}
// 测量表单填写和提交响应时间
await page.click('#open-form-button');
await page.waitForSelector('form.user-form');
const formStartTime = Date.now();
// 填写表单字段
await page.fill('#name', 'Test User');
await page.fill('#email', 'test@example.com');
await page.selectOption('#country', 'US');
await page.fill('#comments', 'This is a performance test');
// 提交表单
await page.click('button[type="submit"]');
// 等待成功消息
await page.waitForSelector('.submission-success');
const formSubmitTime = Date.now() - formStartTime;
console.log(`表单提交总时间: ${formSubmitTime}ms`);
// 性能断言
expect(Math.max(...tabSwitchTimes.map(t => t.time))).toBeLessThan(300); // 标签切换时间小于300ms
expect(formSubmitTime).toBeLessThan(2000); // 表单提交时间小于2秒
});
性能测试最佳实践
- 设置基准指标:为关键页面和操作设置性能基准,并在测试中验证
- 持续监控:定期运行性能测试,查找性能退化
- 测试真实环境:在模拟的生产环境进行测试,包括网络节流
- 测量关键指标:关注核心Web指标(CWV)如LCP(最大内容绘制, Largest Contentful Paint)、FID(首次输入延迟, First Input Delay)和CLS(累积布局偏移, Cumulative Layout Shift)
- 监控资源大小:跟踪JS、CSS和图片资源大小
- 整合CI/CD:将性能测试整合到CI/CD流程中
- 优化关键路径:基于测试结果优化关键渲染路径
5. 高级调试技巧
复杂测试场景需要强大的调试能力,Playwright提供了多种高级调试工具。
使用Trace Viewer
// 启用和使用Trace Viewer
// playwright.config.js中配置
// use: {
// trace: 'on-first-retry', // 首次失败时记录
// }
// 单个测试中启用
test('使用Trace调试复杂交互', async ({ page }) => {
// 启用追踪
await context.tracing.start({
screenshots: true,
snapshots: true
});
// 执行测试步骤
await page.goto('https://example.com/dashboard');
await page.click('.complex-widget');
await page.waitForResponse('**/api/widget-data');
await page.locator('.widget-result').waitFor();
// 断言可能失败的地方
await expect(page.locator('.result-value')).toContainText('Expected Result');
// 测试完成后保存追踪
await context.tracing.stop({ path: 'trace.zip' });
// 访问生成的文件: npx playwright show-trace trace.zip
});
调试特定元素
// 元素调试技巧
test('调试复杂的元素选择器', async ({ page }) => {
await page.goto('https://example.com/complex-ui');
// 暂停执行进行调试
// 仅在无头模式为false时有效
// await page.pause();
// 获取元素详细信息
const element = page.locator('.hard-to-select-element');
// 打印元素信息
const elementInfo = await element.evaluate(el => {
return {
tagName: el.tagName,
id: el.id,
className: el.className,
attributes: Array.from(el.attributes).map(attr => ({
name: attr.name,
value: attr.value
})),
boundingBox: el.getBoundingClientRect(),
isVisible: el.checkVisibility && el.checkVisibility(),
computedStyle: {
display: getComputedStyle(el).display,
visibility: getComputedStyle(el).visibility,
position: getComputedStyle(el).position,
zIndex: getComputedStyle(el).zIndex,
opacity: getComputedStyle(el).opacity
}
};
});
console.log('元素信息:', JSON.stringify(elementInfo, null, 2));
// 高亮元素进行视觉调试
await element.evaluate(el => {
const originalStyle = el.getAttribute('style') || '';
el.setAttribute('style', `${originalStyle}; border: 2px solid red !important; background-color: rgba(255, 0, 0, 0.2) !important;`);
// 5秒后恢复原样式
setTimeout(() => {
el.setAttribute('style', originalStyle);
}, 5000);
});
// 等待高亮效果
await page.waitForTimeout(1000);
// 截图以便后续分析
await page.screenshot({ path: 'debug-element.png' });
// 继续测试
await element.click();
await expect(page.locator('.result')).toBeVisible();
});
输出DOM快照
// 保存DOM快照进行调试
test('在特定状态捕获DOM快照', async ({ page }) => {
await page.goto('https://example.com/dynamic-content');
// 进行一些操作
await page.click('#load-data');
await page.waitForSelector('.data-loaded', { state: 'visible' });
// 捕获某个区域的HTML快照
const containerHtml = await page.locator('#dynamic-container').evaluate(el => {
// 清理可能敏感的数据
const clone = el.cloneNode(true);
const sensitiveElements = clone.querySelectorAll('.user-data, .api-key');
sensitiveElements.forEach(el => {
el.textContent = '[REDACTED]';
});
return clone.outerHTML;
});
// 保存HTML快照
fs.writeFileSync('dom-snapshot.html', `
<!DOCTYPE html>
<html>
<head>
<title>DOM快照 - ${new Date().toLocaleString()}</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.snapshot-container { border: 1px solid #ccc; padding: 15px; margin-top: 20px; }
.timestamp { color: #666; font-style: italic; }
</style>
</head>
<body>
<h1>页面DOM快照</h1>
<p class="timestamp">捕获时间: ${new Date().toLocaleString()}</p>
<p>URL: ${page.url()}</p>
<h2>Container HTML:</h2>
<div class="snapshot-container">
${containerHtml}
</div>
</body>
</html>
`);
console.log('DOM快照已保存到: dom-snapshot.html');
});
调试JavaScript错误
// 捕获并分析JavaScript错误
test('监控并分析JavaScript错误', async ({ page }) => {
// 收集所有JS错误
const jsErrors = [];
// 监听页面错误
page.on('pageerror', error => {
jsErrors.push({
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
console.error('页面错误:', error.message);
});
// 监听控制台错误
page.on('console', msg => {
if (msg.type() === 'error') {
jsErrors.push({
type: 'console',
message: msg.text(),
timestamp: new Date().toISOString()
});
console.error('控制台错误:', msg.text());
}
});
// 访问可能有错误的页面
await page.goto('https://example.com/page-with-errors');
// 触发可能导致错误的操作
await page.click('#error-button').catch(e => {
// 继续执行,我们在收集错误而不是让测试失败
});
// 等待可能的异步错误
await page.waitForTimeout(2000);
// 分析收集到的错误
if (jsErrors.length > 0) {
console.log(`检测到 ${jsErrors.length} 个JavaScript错误`);
// 保存错误日志
fs.writeFileSync('js-errors.json', JSON.stringify(jsErrors, null, 2));
// 对于已知的可接受错误,可以进行过滤
const criticalErrors = jsErrors.filter(err =>
!err.message.includes('已知且可忽略的错误') &&
!err.message.includes('第三方脚本错误')
);
// 只对关键错误进行断言
expect(criticalErrors).toHaveLength(0);
} else {
console.log('未检测到JavaScript错误');
}
});
通过数据库验证测试
// 使用数据库验证测试结果
// 需要安装: npm install -D mysql2
const mysql = require('mysql2/promise');
test('验证表单提交在数据库中创建了记录', async ({ page }) => {
// 连接数据库(使用测试数据库)
const connection = await mysql.createConnection({
host: 'localhost',
user: 'test_user',
password: 'test_password',
database: 'test_db'
});
try {
// 生成唯一标识符以便在数据库中识别
const uniqueId = `test-${Date.now()}`;
// 检查测试前的记录数
const [initialRows] = await connection.execute(
'SELECT COUNT(*) as count FROM contact_submissions WHERE email LIKE ?',
[`%${uniqueId}%`]
);
const initialCount = initialRows[0].count;
// 在页面上执行操作
await page.goto('https://example.com/contact');
// 填写表单
await page.fill('#name', 'Test User');
await page.fill('#email', `user-${uniqueId}@example.com`);
await page.fill('#message', 'This is a test message');
// 提交表单
await page.click('#submit-button');
// 等待成功消息
await expect(page.locator('.success-message')).toBeVisible();
// 等待数据库处理(根据实际情况可能需要调整)
await page.waitForTimeout(1000);
// 验证数据库中的记录
const [finalRows] = await connection.execute(
'SELECT * FROM contact_submissions WHERE email = ?',
[`user-${uniqueId}@example.com`]
);
// 验证记录已创建
expect(finalRows.length).toBe(initialCount + 1);
// 验证记录字段
const submission = finalRows[0];
expect(submission.name).toBe('Test User');
expect(submission.message).toBe('This is a test message');
// 清理测试数据(如果需要)
await connection.execute(
'DELETE FROM contact_submissions WHERE email = ?',
[`user-${uniqueId}@example.com`]
);
} finally {
// 确保关闭数据库连接
await connection.end();
}
});
调试最佳实践
- 使用有意义的快照命名:用描述性名称保存截图和跟踪文件
- 记录上下文信息:捕获环境变量、浏览器版本和测试配置
- 分析测试数据:出现失败时保存DOM状态和网络请求
- 使用隔离的测试环境:避免测试之间的状态污染
- 增量调试:逐步添加复杂度来识别问题根源
- 保留中间状态:在复杂操作间保存状态,以便回溯分析
- 限制重试次数:防止重试掩盖了间歇性问题
6. 跨浏览器测试策略
Web应用需要在各种浏览器中正常工作,Playwright的多浏览器支持使跨浏览器测试变得简单。
配置多浏览器测试
// playwright.config.js中的基本配置
module.exports = {
// 在所有三种浏览器引擎上运行
projects: [
{
name: 'Chromium',
use: { browserName: 'chromium' }
},
{
name: 'Firefox',
use: { browserName: 'firefox' }
},
{
name: 'WebKit',
use: { browserName: 'webkit' }
}
]
};
处理浏览器特定代码
// 处理浏览器差异
test('跨浏览器兼容性测试', async ({ page, browserName }) => {
await page.goto('https://example.com');
// 根据浏览器执行不同的步骤
if (browserName === 'webkit') {
// Safari特定处理
console.log('在Safari/WebKit中执行特定步骤');
await page.click('.safari-compatible-button');
} else if (browserName === 'firefox') {
// Firefox特定处理
console.log('在Firefox中执行特定步骤');
// Firefox可能需要额外的等待时间
await page.waitForTimeout(200);
await page.click('.standard-button');
} else {
// Chromium默认处理
console.log('在Chromium中执行标准步骤');
await page.click('.standard-button');
}
// 通用验证步骤
await expect(page.locator('.result')).toBeVisible();
});
分组浏览器特定测试
// 按浏览器组织测试
// playwright.config.js
module.exports = {
projects: [
// 在所有浏览器上运行的基本测试
{
name: '跨浏览器',
testMatch: /.*basic\.spec\.js/,
use: { browserName: 'chromium' }
},
{
name: '跨浏览器',
testMatch: /.*basic\.spec\.js/,
use: { browserName: 'firefox' }
},
{
name: '跨浏览器',
testMatch: /.*basic\.spec\.js/,
use: { browserName: 'webkit' }
},
// 浏览器特定测试
{
name: 'Chromium特定',
testMatch: /.*chromium\.spec\.js/,
use: { browserName: 'chromium' }
},
{
name: 'Firefox特定',
testMatch: /.*firefox\.spec\.js/,
use: { browserName: 'firefox' }
},
{
name: 'WebKit特定',
testMatch: /.*webkit\.spec\.js/,
use: { browserName: 'webkit' }
}
]
};
检测浏览器功能
// 检测浏览器功能并相应调整测试
test('根据浏览器功能调整测试', async ({ page, browserName }) => {
await page.goto('https://example.com/features');
// 检测浏览器是否支持特定功能
const hasWebP = await page.evaluate(() => {
return document.createElement('canvas')
.toDataURL('image/webp')
.indexOf('data:image/webp') === 0;
});
const hasWebGL = await page.evaluate(() => {
try {
const canvas = document.createElement('canvas');
return !!(window.WebGLRenderingContext &&
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl')));
} catch (e) {
return false;
}
});
// 基于检测到的功能修改测试流程
if (hasWebP) {
console.log('浏览器支持WebP格式');
await page.click('#load-webp-images');
await expect(page.locator('.webp-container img')).toBeVisible();
} else {
console.log('浏览器不支持WebP格式,将使用备用格式');
await page.click('#load-fallback-images');
await expect(page.locator('.fallback-container img')).toBeVisible();
}
if (hasWebGL) {
console.log('浏览器支持WebGL,测试3D功能');
await page.click('#enable-3d-view');
await expect(page.locator('#canvas-3d')).toBeVisible();
} else {
console.log('浏览器不支持WebGL,跳过3D功能测试');
// 跳过WebGL测试,但确保2D备用视图可用
await expect(page.locator('#fallback-2d-view')).toBeVisible();
}
});
视觉比较策略
// 视觉比较策略
test('跨浏览器视觉比较', async ({ page, browserName }) => {
await page.goto('https://example.com/ui-test');
// 为不同浏览器设置不同的比较阈值
let threshold = 0.2; // 默认允许20%像素差异
// 不同浏览器的字体渲染和像素精度有差异
if (browserName === 'firefox') {
threshold = 0.3; // Firefox需要更宽松的阈值
} else if (browserName === 'webkit') {
threshold = 0.25; // WebKit也需要略宽松的阈值
}
// 设置视口大小,确保一致的布局基础
await page.setViewportSize({ width: 1280, height: 720 });
// 截图并与参考图像比较
const screenshot = await page.screenshot();
// 使用浏览器特定的参考图像
const referenceImagePath = `./reference-images/ui-${browserName}.png`;
// 使用图像比较库进行比较(示例)
// 实际使用时可替换为Playwright的toMatchSnapshot
expect(screenshot).toMatchSnapshot(referenceImagePath, {
threshold,
maxDiffPixelRatio: 0.05 // 最多允许5%的像素差异
});
});
浏览器版本矩阵
// 使用特定版本的浏览器(使用Docker容器示例)
// 需要安装: npm install -D dockerode
const { test, expect } = require('@playwright/test');
const Docker = require('dockerode');
const docker = new Docker();
/**
* 在特定版本的浏览器中运行测试
* @param {string} browserType - 浏览器类型,如'chromium'、'firefox'、'webkit'
* @param {string} version - 浏览器版本号,如'101'、'99'
* @param {Function} testFn - 测试函数,将在指定浏览器中执行
* @returns {Object} - 测试结果对象,包含success和output属性
*/
async function runTestInBrowserVersion(browserType, version, testFn) {
// 容器名称
const containerName = `playwright-${browserType}-${version}`;
// 拉取特定版本的浏览器Docker镜像
console.log(`拉取 ${browserType} ${version} 镜像...`);
// 确定Docker镜像名称
const imageName = `mcr.microsoft.com/playwright:v1.32.0-${browserType}-${version}`;
try {
// 创建容器
const container = await docker.createContainer({
Image: imageName,
name: containerName,
Cmd: ['npx', 'playwright', 'test'],
// 挂载当前目录
HostConfig: {
Binds: [`${process.cwd()}:/app`]
},
WorkingDir: '/app'
});
// 启动容器
await container.start();
// 执行测试
const exec = await container.exec({
Cmd: ['node', '-e', testFn.toString()],
AttachStdout: true,
AttachStderr: true
});
// 启动执行
const stream = await exec.start();
// 读取输出
let output = '';
stream.on('data', chunk => {
output += chunk.toString();
});
// 等待执行完成
await new Promise(resolve => stream.on('end', resolve));
// 停止并移除容器
await container.stop();
await container.remove();
return { success: !output.includes('Error'), output };
} catch (error) {
console.error(`在 ${browserType} ${version} 中运行测试失败:`, error);
return { success: false, output: error.toString() };
}
}
// 使用示例
test('跨多个浏览器版本测试', async () => {
// 测试函数
const testFunction = async () => {
const { chromium } = require('playwright');
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// 测试逻辑
const title = await page.title();
console.log(`页面标题: ${title}`);
await browser.close();
};
// 在多个Chrome版本上运行
// 注意: 这些版本号需要根据Docker镜像的可用性进行调整
const versions = ['101', '99', '95'];
const testResults = {};
for (const version of versions) {
console.log(`在Chrome ${version}中测试...`);
testResults[version] = await runTestInBrowserVersion('chromium', version, testFunction);
console.log(`Chrome ${version}测试结果:`, testResults[version].success ? '通过' : '失败');
}
// 验证所有版本测试都成功
const failedVersions = Object.entries(testResults)
.filter(([_, result]) => !result.success)
.map(([version]) => version);
expect(failedVersions).toHaveLength(0,
`在以下Chrome版本中测试失败: ${failedVersions.join(', ')}`);
});
跨浏览器测试最佳实践
- 确定关键浏览器:根据用户数据确定优先测试哪些浏览器
- 分层测试策略:所有浏览器上运行核心功能测试,专用浏览器上运行特定测试
- 使用特性检测:基于浏览器能力而非浏览器标识来调整测试
- 维护单独的基准图像:为每个浏览器保留单独的视觉比较参考图像
- 编写跨浏览器兼容代码:避免使用浏览器专有API,或提供回退方案
- 自动化浏览器差异处理:使用辅助函数隐藏浏览器间的差异
- 监控浏览器覆盖率:确保未忽略重要的浏览器或版本
总结
通过本文详细介绍的Playwright进阶技术,您可以构建更加强大、可靠的自动化测试套件,更好地保障应用程序的质量。
我们涵盖了以下进阶主题:
- 截图与视频录制:通过视觉证据捕获测试执行,便于问题诊断和复现
- 测试框架集成:将Playwright与Jest、Mocha和Cucumber等框架无缝集成
- 移动设备模拟:确保应用在各种移动设备上正常工作
- 网络请求拦截与模拟:测试应用对不同API响应的处理能力
- 并行测试执行:通过并行化显著提高测试执行速度
- 身份验证管理:高效处理需要登录的应用测试场景
- 高级事件处理:测试WebSocket、Service Worker等复杂异步行为
- 可访问性测试:确保应用对所有用户可用
- 性能测试与监控:识别和解决性能瓶颈
- 高级调试技巧:使用追踪和状态捕获调试复杂问题
- 跨浏览器测试策略:确保应用在所有目标浏览器上工作