Playwright自动化测试实战指南-中级部分
Playwright自动化测试实战指南-中级部分
版本说明:本文基于 Playwright v1.32.0 版本,介绍了自动化测试的进阶应用场景和技巧。由于Playwright更新较快,部分API可能在新版本中有所变化,建议同时参考官方文档获取最新信息。
引言
随着Web应用日益复杂,自动化测试变得愈发重要。Playwright作为一款现代化的自动化测试工具,提供了强大的跨浏览器测试能力。本文适合已经掌握Playwright基础知识,希望进一步提升测试技能的开发者和测试工程师阅读。我们将深入探讨Playwright的进阶应用,帮助你构建更加健壮、高效的测试方案。
目录
- 中级应用
- 1. 截图与视频录制
- 2. 测试框架集成
- 3. 移动设备模拟
- 4. 网络请求拦截与模拟
- 5. 并行测试执行
- 高级应用
- 1. 身份验证管理
- 2. 高级事件处理
- 3. 可访问性测试
- 4. 性能测试与监控
- 5. 高级调试技巧
- 6. 跨浏览器测试策略
- 总结
- 学习资源
中级应用
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 Workerpage.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-launcherconst 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 () => {// 启动Chromeconst 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); // 性能得分>=70expect(result.lhr.categories.accessibility.score).toBeGreaterThanOrEqual(0.9); // 可访问性得分>=90// 关闭Chromeawait 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); // 标签切换时间小于300msexpect(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的toMatchSnapshotexpect(screenshot).toMatchSnapshot(referenceImagePath, {threshold,maxDiffPixelRatio: 0.05 // 最多允许5%的像素差异});
});
浏览器版本矩阵
// 使用特定版本的浏览器(使用Docker容器示例)
// 需要安装: npm install -D dockerodeconst { 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等复杂异步行为
- 可访问性测试:确保应用对所有用户可用
- 性能测试与监控:识别和解决性能瓶颈
- 高级调试技巧:使用追踪和状态捕获调试复杂问题
- 跨浏览器测试策略:确保应用在所有目标浏览器上工作