作为一名长期从事Web自动化测试的工程师,我见证了从Selenium到Playwright的技术演进。Playwright作为微软开源的现代化浏览器自动化工具,凭借其跨浏览器支持、可靠的选择器定位和强大的录制功能,正在成为自动化测试领域的新宠。
Playwright最吸引我的特性是它原生支持Chromium、WebKit和Firefox三大浏览器引擎,这意味着我们可以在不同浏览器上运行完全一致的测试脚本。与传统的Selenium相比,Playwright的API设计更加现代化,执行速度更快,而且内置了自动等待机制,大大减少了测试脚本中的flaky问题。
在实际项目中,我发现Playwright特别适合以下场景:
首先需要安装Playwright CLI工具:
bash复制npm install -g @playwright/test
然后初始化一个测试项目:
bash复制mkdir playwright-demo && cd playwright-demo
npm init playwright@latest
启动录制模式:
bash复制npx playwright codegen
这个命令会打开两个窗口:一个是浏览器窗口,一个是代码生成器窗口。在浏览器中的所有操作都会被实时转换为代码。
提示:建议在开始录制前先登录系统或准备好测试环境,避免将敏感凭证信息记录在脚本中。
慢动作模式:通过--slow-mo参数可以放慢操作速度,便于观察录制过程
bash复制npx playwright codegen --slow-mo 1000
设备模拟:可以模拟特定移动设备进行录制
bash复制npx playwright codegen --device="iPhone 13"
语言选择:支持生成Python、Java、C#等多种语言的脚本
bash复制npx playwright codegen --target=python
在实际项目中,我通常会先使用录制功能快速生成脚本框架,然后手动优化定位器和添加断言。这种半自动化的方式能显著提高脚本开发效率。
Playwright提供了多种强大的定位策略:
文本定位:通过元素文本内容定位
javascript复制await page.click('text=登录')
CSS选择器:传统的CSS选择器定位
javascript复制await page.click('#submit-button')
XPath:复杂的XPath表达式定位
javascript复制await page.click('//button[@id="submit"]')
角色定位:通过ARIA角色定位
javascript复制await page.click('role=button[name="Submit"]')
布局定位:基于元素位置关系定位
javascript复制await page.click('button:right-of(#cancel-button)')
优先使用角色定位:ARIA角色定位是最稳定的方式,不受样式变化影响
避免使用索引定位:类似nth-child(2)这样的定位方式极易失效
组合定位策略:可以组合多种定位方式提高稳定性
javascript复制await page.click('role=button[name="Submit"] >> #submit-form')
自定义属性:与开发团队协商添加测试专用属性
html复制<button data-testid="submit-btn">Submit</button>
javascript复制await page.click('[data-testid="submit-btn"]')
在我的项目中,通过采用这些定位策略,元素定位的稳定性提升了80%以上,大大减少了测试脚本的维护成本。
当元素定位失败时,可以按照以下步骤排查:
验证选择器:在DevTools控制台使用$$('your-selector')验证选择器是否有效
检查iframe:目标元素可能在iframe中,需要先切换到对应frame
javascript复制const frame = page.frame('frame-name');
await frame.click('button');
处理动态内容:对于动态生成的元素,使用page.waitForSelector()
javascript复制await page.waitForSelector('#dynamic-element', { state: 'visible' });
检查页面状态:确保操作前页面已完全加载
javascript复制await page.waitForLoadState('networkidle');
去除冗余操作:录制生成的脚本通常包含不必要的等待和导航操作
添加明确等待:替换隐式等待为显式等待条件
javascript复制// 不推荐
await page.waitForTimeout(5000);
// 推荐
await page.waitForSelector('#success-message', { state: 'visible' });
参数化测试数据:将硬编码的值提取为变量或配置文件
javascript复制const testData = require('./test-data.json');
await page.fill('#username', testData.username);
添加错误处理:增加try-catch块和失败截图
javascript复制try {
await page.click('text=Submit');
} catch (error) {
await page.screenshot({ path: 'error.png' });
throw error;
}
Playwright可以模拟各种复杂的用户交互:
文件上传:
javascript复制await page.setInputFiles('input[type="file"]', 'test-file.pdf');
拖放操作:
javascript复制await page.dragAndDrop('#source', '#target');
键盘操作:
javascript复制await page.keyboard.press('Control+A');
鼠标移动:
javascript复制await page.mouse.move(100, 200);
Playwright强大的网络API可以拦截和修改请求:
javascript复制await page.route('**/api/*', route => {
if (route.request().url().includes('user-data')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ mock: 'data' })
});
} else {
route.continue();
}
});
这个功能特别适合:
结合截图比对功能实现视觉回归测试:
javascript复制const screenshot = await page.screenshot();
expect(screenshot).toMatchSnapshot('homepage.png');
在实际项目中,我通常会设置5%的像素差异阈值,避免因字体渲染差异导致的误报:
javascript复制expect(screenshot).toMatchSnapshot('homepage.png', {
threshold: 0.05,
maxDiffPixels: 100
});
Playwright Test支持测试并行执行,大幅缩短测试套件运行时间:
javascript复制// playwright.config.js
module.exports = {
workers: process.env.CI ? 4 : 2
};
注意:并行测试需要确保测试用例相互独立,不共享状态
通过复用浏览器上下文减少启动开销:
javascript复制const browser = await chromium.launch();
const context = await browser.newContext();
// 在多个测试中复用context
const page1 = await context.newPage();
const page2 = await context.newPage();
避免使用固定等待时间,采用智能等待条件:
javascript复制// 等待网络请求完成
await page.waitForResponse(response =>
response.url().includes('/api/data') &&
response.status() === 200
);
// 等待元素状态
await page.waitForFunction(
selector => document.querySelector(selector).disabled,
'#submit-button'
);
在GitHub Actions中集成Playwright测试的示例:
yaml复制name: Playwright Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm ci
- run: npx playwright install
- run: npx playwright test
- uses: actions/upload-artifact@v2
if: always()
with:
name: playwright-report
path: playwright-report/
Playwright支持多种格式的测试报告:
HTML报告:
bash复制npx playwright show-report
JUnit报告:
javascript复制// playwright.config.js
module.exports = {
reporter: [['junit', { outputFile: 'results.xml' }]]
};
Allure报告:
javascript复制module.exports = {
reporter: [['allure-playwright']]
};
在实际项目中,HTML报告因其丰富的交互性和可视化效果最受欢迎,特别适合在团队中分享测试结果。
Playwright支持精确模拟移动设备:
javascript复制const { devices } = require('@playwright/test');
const iPhone = devices['iPhone 12'];
const browser = await chromium.launch();
const context = await browser.newContext({
...iPhone,
locale: 'zh-CN',
timezoneId: 'Asia/Shanghai'
});
针对移动端特有的触摸操作:
javascript复制// 长按
await page.touchscreen.tap(100, 200);
await page.waitForTimeout(1000);
// 滑动
await page.touchscreen.down(100, 200);
await page.touchscreen.move(150, 200);
await page.touchscreen.up();
视口测试:
javascript复制expect(await page.evaluate(() => window.innerWidth)).toBe(375);
触摸支持检测:
javascript复制expect(await page.evaluate(() => 'ontouchstart' in window)).toBeTruthy();
设备方向模拟:
javascript复制await context.setGeolocation({ latitude: 39.9042, longitude: 116.4074 });
利用Playwright检测XSS漏洞:
javascript复制const maliciousInput = '<script>alert("XSS")</script>';
await page.fill('#comment-input', maliciousInput);
await page.click('#submit-button');
const dialog = await page.waitForEvent('dialog');
expect(dialog.message()).toContain('XSS');
测试CSRF防护机制:
javascript复制// 尝试不带CSRF token提交表单
await page.evaluate(() => {
fetch('/api/sensitive-action', {
method: 'POST',
body: JSON.stringify({ key: 'value' })
});
});
const response = await page.waitForResponse('/api/sensitive-action');
expect(response.status()).toBe(403);
验证不同角色的访问权限:
javascript复制// 管理员上下文
const adminContext = await browser.newContext({
storageState: 'admin-storage-state.json'
});
// 普通用户上下文
const userContext = await browser.newContext({
storageState: 'user-storage-state.json'
});
// 验证管理员专属页面
const adminPage = await adminContext.newPage();
await adminPage.goto('/admin-panel');
expect(adminPage.url()).toContain('/admin-panel');
// 验证普通用户无法访问
const userPage = await userContext.newPage();
await userPage.goto('/admin-panel');
expect(userPage.url()).not.toContain('/admin-panel');
定位器版本控制:将关键定位器提取到单独的文件中管理
javascript复制// locators.js
module.exports = {
login: {
username: '#username',
password: '#password',
submit: 'role=button[name="登录"]'
}
};
定位器健康检查:定期运行定位器验证脚本
javascript复制const locators = require('./locators');
for (const [pageName, pageLocators] of Object.entries(locators)) {
test(`验证${pageName}页面定位器`, async ({ page }) => {
await page.goto(`/${pageName}`);
for (const [elementName, selector] of Object.entries(pageLocators)) {
await expect(page.locator(selector)).toBeVisible();
}
});
}
工厂模式生成测试数据:
javascript复制class UserFactory {
static create(role = 'user') {
const baseUser = {
username: `user_${Math.random().toString(36).substring(2, 8)}`,
password: 'Test1234!'
};
if (role === 'admin') {
return { ...baseUser, permissions: ['read', 'write', 'admin'] };
}
return baseUser;
}
}
测试数据清理:
javascript复制afterEach(async ({ request }) => {
await request.post('/api/test/cleanup', {
data: { testRunId: process.env.TEST_RUN_ID }
});
});
按业务域划分:
code复制tests/
├── auth/
├── checkout/
├── profile/
└── admin/
按测试类型划分:
code复制tests/
├── functional/
├── integration/
└── visual/
混合策略:
code复制tests/
├── smoke/
├── regression/
└── e2e/
├── auth/
└── checkout/
在实际项目中,我倾向于采用混合策略,既能保持组织结构清晰,又能灵活应对不同类型的测试需求。