Playwright使用
📋 概述
Playwright是由Microsoft开发的现代化端到端测试框架,支持Chromium、Firefox、Safari等多种浏览器。它提供了强大的自动化测试能力,包括API测试、UI测试、移动端测试等,是构建可靠Web应用测试的首选工具之一。
🎯 学习目标
- 掌握Playwright的安装、配置和基本使用
- 学会编写稳定可靠的E2E测试用例
- 了解Playwright的高级功能和最佳实践
- 掌握测试调试、并行执行和CI/CD集成
🚀 Playwright环境搭建
安装和初始化
bash
# 创建新项目
mkdir playwright-testing
cd playwright-testing
npm init -y
# 安装Playwright
npm install -D @playwright/test
# 初始化Playwright配置
npx playwright install
# 安装浏览器(可选,如果之前没有安装)
npx playwright install chromium firefox webkit
基础配置
javascript
// playwright.config.js
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// 测试目录
testDir: './tests',
// 全局测试超时时间
timeout: 30 * 1000,
// 期望超时时间
expect: {
timeout: 5000
},
// 失败时重试次数
retries: process.env.CI ? 2 : 0,
// 并行执行的工作进程数
workers: process.env.CI ? 1 : undefined,
// 报告器配置
reporter: [
['html'],
['json', { outputFile: 'playwright-report.json' }],
['junit', { outputFile: 'playwright-results.xml' }]
],
// 全局设置
use: {
// 基础URL
baseURL: 'http://localhost:3000',
// 浏览器追踪
trace: 'on-first-retry',
// 截图设置
screenshot: 'only-on-failure',
// 视频录制
video: 'retain-on-failure',
// 忽略HTTPS错误
ignoreHTTPSErrors: true,
// 用户代理
userAgent: 'Playwright Test Agent'
},
// 项目配置(多浏览器测试)
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 12'] },
}
],
// Web服务器配置
webServer: {
command: 'npm run start',
port: 3000,
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
// 输出目录
outputDir: 'test-results/',
});
环境变量配置
javascript
// .env
PLAYWRIGHT_BASE_URL=http://localhost:3000
PLAYWRIGHT_HEADLESS=true
PLAYWRIGHT_BROWSER=chromium
PLAYWRIGHT_TIMEOUT=30000
PLAYWRIGHT_RETRIES=2
CI=false
📝 基础测试编写
简单页面测试
javascript
// tests/basic-navigation.spec.js
import { test, expect } from '@playwright/test';
test.describe('基础页面导航测试', () => {
test.beforeEach(async ({ page }) => {
// 每个测试前访问首页
await page.goto('/');
});
test('应该正确加载首页', async ({ page }) => {
// 验证页面标题
await expect(page).toHaveTitle(/Welcome/);
// 验证主要元素存在
await expect(page.locator('h1')).toContainText('Welcome');
await expect(page.locator('nav')).toBeVisible();
await expect(page.locator('footer')).toBeVisible();
});
test('应该能够导航到不同页面', async ({ page }) => {
// 点击导航链接
await page.click('nav a[href="/about"]');
// 验证URL变化
await expect(page).toHaveURL(/.*about/);
// 验证页面内容
await expect(page.locator('h1')).toContainText('About');
});
test('应该在移动端正确显示', async ({ page, isMobile }) => {
if (isMobile) {
// 验证移动端菜单按钮
await expect(page.locator('.mobile-menu-toggle')).toBeVisible();
// 点击移动端菜单
await page.click('.mobile-menu-toggle');
await expect(page.locator('.mobile-menu')).toBeVisible();
} else {
// 验证桌面端导航
await expect(page.locator('.desktop-nav')).toBeVisible();
}
});
});
表单交互测试
javascript
// tests/form-interactions.spec.js
import { test, expect } from '@playwright/test';
test.describe('表单交互测试', () => {
test('用户注册流程', async ({ page }) => {
await page.goto('/register');
// 填写注册表单
await page.fill('[data-testid="username"]', 'testuser123');
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'SecurePass123!');
await page.fill('[data-testid="confirm-password"]', 'SecurePass123!');
// 选择用户类型
await page.selectOption('[data-testid="user-type"]', 'premium');
// 勾选同意条款
await page.check('[data-testid="terms-checkbox"]');
// 提交表单
await page.click('[data-testid="submit-button"]');
// 验证成功消息
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
await expect(page.locator('[data-testid="success-message"]')).toContainText('注册成功');
// 验证重定向到仪表板
await expect(page).toHaveURL(/.*dashboard/);
});
test('表单验证错误处理', async ({ page }) => {
await page.goto('/register');
// 提交空表单
await page.click('[data-testid="submit-button"]');
// 验证错误消息
await expect(page.locator('[data-testid="username-error"]')).toBeVisible();
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
await expect(page.locator('[data-testid="password-error"]')).toBeVisible();
// 填写无效邮箱
await page.fill('[data-testid="email"]', 'invalid-email');
await page.click('[data-testid="submit-button"]');
await expect(page.locator('[data-testid="email-error"]'))
.toContainText('请输入有效的邮箱地址');
});
test('文件上传功能', async ({ page }) => {
await page.goto('/profile');
// 监听文件上传请求
const uploadPromise = page.waitForResponse(resp =>
resp.url().includes('/api/upload') && resp.status() === 200
);
// 上传文件
const fileInput = page.locator('[data-testid="avatar-upload"]');
await fileInput.setInputFiles('./test-fixtures/avatar.jpg');
// 等待上传完成
await uploadPromise;
// 验证上传成功
await expect(page.locator('[data-testid="upload-success"]')).toBeVisible();
await expect(page.locator('[data-testid="avatar-preview"]')).toBeVisible();
});
});
API测试集成
javascript
// tests/api-integration.spec.js
import { test, expect } from '@playwright/test';
test.describe('API集成测试', () => {
let apiContext;
let authToken;
test.beforeAll(async ({ playwright }) => {
// 创建API上下文
apiContext = await playwright.request.newContext({
baseURL: 'http://localhost:3000/api',
extraHTTPHeaders: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
// 获取认证令牌
const loginResponse = await apiContext.post('/auth/login', {
data: {
email: 'test@example.com',
password: 'testpassword'
}
});
const loginData = await loginResponse.json();
authToken = loginData.token;
});
test.afterAll(async () => {
await apiContext.dispose();
});
test('获取用户列表', async () => {
const response = await apiContext.get('/users', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.status()).toBe(200);
const users = await response.json();
expect(Array.isArray(users)).toBeTruthy();
expect(users.length).toBeGreaterThan(0);
// 验证用户数据结构
expect(users[0]).toHaveProperty('id');
expect(users[0]).toHaveProperty('name');
expect(users[0]).toHaveProperty('email');
});
test('创建新用户', async () => {
const newUser = {
name: 'New Test User',
email: `test-${Date.now()}@example.com`,
password: 'newpassword123'
};
const response = await apiContext.post('/users', {
headers: {
'Authorization': `Bearer ${authToken}`
},
data: newUser
});
expect(response.status()).toBe(201);
const createdUser = await response.json();
expect(createdUser.name).toBe(newUser.name);
expect(createdUser.email).toBe(newUser.email);
expect(createdUser).toHaveProperty('id');
expect(createdUser).not.toHaveProperty('password'); // 密码不应返回
});
test('处理API错误', async () => {
// 测试无效数据
const response = await apiContext.post('/users', {
headers: {
'Authorization': `Bearer ${authToken}`
},
data: {
name: '', // 空名称
email: 'invalid-email' // 无效邮箱
}
});
expect(response.status()).toBe(400);
const errorData = await response.json();
expect(errorData).toHaveProperty('errors');
expect(Array.isArray(errorData.errors)).toBeTruthy();
});
test('UI与API数据一致性', async ({ page }) => {
// 通过API获取用户数据
const apiResponse = await apiContext.get('/users', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
const apiUsers = await apiResponse.json();
// 访问用户列表页面
await page.goto('/users');
// 等待数据加载
await page.waitForSelector('[data-testid="user-list"]');
// 获取页面显示的用户数量
const userRows = await page.locator('[data-testid="user-row"]').count();
// 验证UI显示的数据与API返回的数据一致
expect(userRows).toBe(apiUsers.length);
// 验证第一个用户的详细信息
if (apiUsers.length > 0) {
const firstUser = apiUsers[0];
await expect(page.locator(`[data-testid="user-${firstUser.id}"] .user-name`))
.toContainText(firstUser.name);
await expect(page.locator(`[data-testid="user-${firstUser.id}"] .user-email`))
.toContainText(firstUser.email);
}
});
});
🔧 高级功能使用
页面对象模式
javascript
// page-objects/base-page.js
export class BasePage {
constructor(page) {
this.page = page;
}
async goto(path = '') {
await this.page.goto(path);
}
async waitForLoadState(state = 'networkidle') {
await this.page.waitForLoadState(state);
}
async takeScreenshot(name) {
await this.page.screenshot({
path: `screenshots/${name}.png`,
fullPage: true
});
}
async getTitle() {
return await this.page.title();
}
async isElementVisible(selector) {
try {
await this.page.waitForSelector(selector, { timeout: 5000 });
return await this.page.isVisible(selector);
} catch {
return false;
}
}
async clickElement(selector) {
await this.page.waitForSelector(selector);
await this.page.click(selector);
}
async fillInput(selector, text) {
await this.page.waitForSelector(selector);
await this.page.fill(selector, text);
}
async selectOption(selector, value) {
await this.page.waitForSelector(selector);
await this.page.selectOption(selector, value);
}
}
javascript
// page-objects/login-page.js
import { BasePage } from './base-page.js';
export class LoginPage extends BasePage {
constructor(page) {
super(page);
// 页面元素选择器
this.selectors = {
emailInput: '[data-testid="email-input"]',
passwordInput: '[data-testid="password-input"]',
loginButton: '[data-testid="login-button"]',
errorMessage: '[data-testid="error-message"]',
rememberMeCheckbox: '[data-testid="remember-me"]',
forgotPasswordLink: '[data-testid="forgot-password"]',
registerLink: '[data-testid="register-link"]'
};
}
async goto() {
await super.goto('/login');
await this.waitForLoadState();
}
async login(email, password, rememberMe = false) {
await this.fillInput(this.selectors.emailInput, email);
await this.fillInput(this.selectors.passwordInput, password);
if (rememberMe) {
await this.clickElement(this.selectors.rememberMeCheckbox);
}
await this.clickElement(this.selectors.loginButton);
}
async getErrorMessage() {
if (await this.isElementVisible(this.selectors.errorMessage)) {
return await this.page.textContent(this.selectors.errorMessage);
}
return null;
}
async clickForgotPassword() {
await this.clickElement(this.selectors.forgotPasswordLink);
}
async clickRegister() {
await this.clickElement(this.selectors.registerLink);
}
async isLoginFormVisible() {
return await this.isElementVisible(this.selectors.emailInput) &&
await this.isElementVisible(this.selectors.passwordInput) &&
await this.isElementVisible(this.selectors.loginButton);
}
}
javascript
// page-objects/dashboard-page.js
import { BasePage } from './base-page.js';
export class DashboardPage extends BasePage {
constructor(page) {
super(page);
this.selectors = {
welcomeMessage: '[data-testid="welcome-message"]',
userMenu: '[data-testid="user-menu"]',
logoutButton: '[data-testid="logout-button"]',
notificationBell: '[data-testid="notification-bell"]',
sidebarToggle: '[data-testid="sidebar-toggle"]',
mainContent: '[data-testid="main-content"]',
statsCards: '[data-testid="stats-card"]',
recentActivity: '[data-testid="recent-activity"]'
};
}
async goto() {
await super.goto('/dashboard');
await this.waitForLoadState();
}
async waitForDashboardLoad() {
await this.page.waitForSelector(this.selectors.welcomeMessage);
await this.page.waitForSelector(this.selectors.mainContent);
}
async getWelcomeMessage() {
return await this.page.textContent(this.selectors.welcomeMessage);
}
async openUserMenu() {
await this.clickElement(this.selectors.userMenu);
await this.page.waitForSelector(this.selectors.logoutButton);
}
async logout() {
await this.openUserMenu();
await this.clickElement(this.selectors.logoutButton);
await this.page.waitForURL(/.*login/);
}
async getNotificationCount() {
const badge = this.page.locator(`${this.selectors.notificationBell} .badge`);
if (await badge.isVisible()) {
return parseInt(await badge.textContent());
}
return 0;
}
async getStatsData() {
const statsCards = this.page.locator(this.selectors.statsCards);
const count = await statsCards.count();
const stats = [];
for (let i = 0; i < count; i++) {
const card = statsCards.nth(i);
const title = await card.locator('.stats-title').textContent();
const value = await card.locator('.stats-value').textContent();
stats.push({ title, value });
}
return stats;
}
async toggleSidebar() {
await this.clickElement(this.selectors.sidebarToggle);
}
}
使用页面对象的测试
javascript
// tests/user-workflow.spec.js
import { test, expect } from '@playwright/test';
import { LoginPage } from '../page-objects/login-page.js';
import { DashboardPage } from '../page-objects/dashboard-page.js';
test.describe('用户工作流测试', () => {
let loginPage;
let dashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
});
test('完整登录到仪表板流程', async () => {
// 访问登录页面
await loginPage.goto();
// 验证登录表单可见
expect(await loginPage.isLoginFormVisible()).toBe(true);
// 执行登录
await loginPage.login('user@example.com', 'password123', true);
// 等待仪表板加载
await dashboardPage.waitForDashboardLoad();
// 验证欢迎消息
const welcomeMessage = await dashboardPage.getWelcomeMessage();
expect(welcomeMessage).toContain('欢迎');
// 验证统计数据加载
const stats = await dashboardPage.getStatsData();
expect(stats.length).toBeGreaterThan(0);
// 验证通知功能
const notificationCount = await dashboardPage.getNotificationCount();
expect(typeof notificationCount).toBe('number');
});
test('登录失败处理', async () => {
await loginPage.goto();
// 尝试使用错误凭据登录
await loginPage.login('wrong@example.com', 'wrongpassword');
// 验证错误消息
const errorMessage = await loginPage.getErrorMessage();
expect(errorMessage).toBeTruthy();
expect(errorMessage).toContain('用户名或密码错误');
});
test('注销流程', async () => {
// 先登录
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await dashboardPage.waitForDashboardLoad();
// 执行注销
await dashboardPage.logout();
// 验证重定向到登录页面
expect(await loginPage.isLoginFormVisible()).toBe(true);
});
});
数据驱动测试
javascript
// tests/data-driven.spec.js
import { test, expect } from '@playwright/test';
import { LoginPage } from '../page-objects/login-page.js';
// 测试数据
const loginTestCases = [
{
description: '有效凭据登录',
email: 'valid@example.com',
password: 'validpassword',
expectedResult: 'success'
},
{
description: '无效邮箱登录',
email: 'invalid@example.com',
password: 'validpassword',
expectedResult: 'error',
expectedMessage: '用户不存在'
},
{
description: '错误密码登录',
email: 'valid@example.com',
password: 'wrongpassword',
expectedResult: 'error',
expectedMessage: '密码错误'
},
{
description: '空邮箱登录',
email: '',
password: 'validpassword',
expectedResult: 'validation',
expectedMessage: '请输入邮箱'
},
{
description: '空密码登录',
email: 'valid@example.com',
password: '',
expectedResult: 'validation',
expectedMessage: '请输入密码'
}
];
test.describe('数据驱动登录测试', () => {
loginTestCases.forEach(testCase => {
test(testCase.description, async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(testCase.email, testCase.password);
switch (testCase.expectedResult) {
case 'success':
// 验证登录成功
await expect(page).toHaveURL(/.*dashboard/);
break;
case 'error':
case 'validation':
// 验证错误消息
const errorMessage = await loginPage.getErrorMessage();
expect(errorMessage).toContain(testCase.expectedMessage);
break;
}
});
});
});
// 参数化测试的另一种方式
const userRoles = ['admin', 'user', 'manager', 'guest'];
userRoles.forEach(role => {
test(`${role}角色权限测试`, async ({ page }) => {
// 根据角色设置不同的测试逻辑
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(`${role}@example.com`, 'password123');
// 验证不同角色的权限
await page.goto('/admin');
if (role === 'admin') {
await expect(page.locator('[data-testid="admin-panel"]')).toBeVisible();
} else {
await expect(page.locator('[data-testid="access-denied"]')).toBeVisible();
}
});
});
🎭 高级测试技巧
浏览器上下文和存储
javascript
// tests/browser-context.spec.js
import { test, expect } from '@playwright/test';
test.describe('浏览器上下文测试', () => {
test('多用户会话测试', async ({ browser }) => {
// 创建两个独立的浏览器上下文
const context1 = await browser.newContext();
const context2 = await browser.newContext();
const page1 = await context1.newPage();
const page2 = await context2.newPage();
try {
// 用户1登录
await page1.goto('/login');
await page1.fill('[data-testid="email"]', 'user1@example.com');
await page1.fill('[data-testid="password"]', 'password1');
await page1.click('[data-testid="login-button"]');
// 用户2登录
await page2.goto('/login');
await page2.fill('[data-testid="email"]', 'user2@example.com');
await page2.fill('[data-testid="password"]', 'password2');
await page2.click('[data-testid="login-button"]');
// 验证两个用户都成功登录
await expect(page1).toHaveURL(/.*dashboard/);
await expect(page2).toHaveURL(/.*dashboard/);
// 验证用户身份独立
const user1Name = await page1.locator('[data-testid="user-name"]').textContent();
const user2Name = await page2.locator('[data-testid="user-name"]').textContent();
expect(user1Name).toContain('用户1');
expect(user2Name).toContain('用户2');
} finally {
await context1.close();
await context2.close();
}
});
test('本地存储测试', async ({ page }) => {
await page.goto('/settings');
// 设置用户偏好
await page.selectOption('[data-testid="theme-select"]', 'dark');
await page.selectOption('[data-testid="language-select"]', 'zh-CN');
await page.click('[data-testid="save-settings"]');
// 验证本地存储
const theme = await page.evaluate(() => localStorage.getItem('theme'));
const language = await page.evaluate(() => localStorage.getItem('language'));
expect(theme).toBe('dark');
expect(language).toBe('zh-CN');
// 刷新页面验证设置持久化
await page.reload();
await expect(page.locator('[data-testid="theme-select"]')).toHaveValue('dark');
await expect(page.locator('[data-testid="language-select"]')).toHaveValue('zh-CN');
});
test('Cookie管理测试', async ({ context, page }) => {
await page.goto('/');
// 设置cookie
await context.addCookies([
{
name: 'session_id',
value: 'test-session-123',
domain: 'localhost',
path: '/'
},
{
name: 'user_preferences',
value: JSON.stringify({ theme: 'dark', lang: 'zh' }),
domain: 'localhost',
path: '/'
}
]);
await page.reload();
// 验证cookie被正确设置
const cookies = await context.cookies();
const sessionCookie = cookies.find(c => c.name === 'session_id');
const prefCookie = cookies.find(c => c.name === 'user_preferences');
expect(sessionCookie.value).toBe('test-session-123');
expect(prefCookie.value).toContain('dark');
});
});
网络拦截和模拟
javascript
// tests/network-mocking.spec.js
import { test, expect } from '@playwright/test';
test.describe('网络拦截测试', () => {
test('API响应模拟', async ({ page }) => {
// 拦截API请求并返回模拟数据
await page.route('/api/users', async route => {
const mockUsers = [
{ id: 1, name: 'Mock User 1', email: 'mock1@example.com' },
{ id: 2, name: 'Mock User 2', email: 'mock2@example.com' }
];
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockUsers)
});
});
await page.goto('/users');
// 验证模拟数据正确显示
await expect(page.locator('[data-testid="user-1"]')).toContainText('Mock User 1');
await expect(page.locator('[data-testid="user-2"]')).toContainText('Mock User 2');
});
test('网络错误模拟', async ({ page }) => {
// 模拟网络错误
await page.route('/api/users', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' })
});
});
await page.goto('/users');
// 验证错误处理
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]'))
.toContainText('加载失败');
});
test('慢速网络模拟', async ({ page }) => {
// 模拟慢速响应
await page.route('/api/users', async route => {
// 延迟3秒
await new Promise(resolve => setTimeout(resolve, 3000));
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([])
});
});
await page.goto('/users');
// 验证加载状态显示
await expect(page.locator('[data-testid="loading-spinner"]')).toBeVisible();
// 等待加载完成
await expect(page.locator('[data-testid="loading-spinner"]')).toBeHidden();
await expect(page.locator('[data-testid="user-list"]')).toBeVisible();
});
test('请求拦截和修改', async ({ page }) => {
// 拦截并修改请求
await page.route('/api/users', async (route, request) => {
// 获取原始请求
const headers = request.headers();
// 添加自定义头部
headers['X-Test-Header'] = 'test-value';
// 继续原始请求但使用修改后的头部
await route.continue({ headers });
});
// 监听请求以验证修改
page.on('request', request => {
if (request.url().includes('/api/users')) {
expect(request.headers()['x-test-header']).toBe('test-value');
}
});
await page.goto('/users');
});
test('文件下载拦截', async ({ page }) => {
// 拦截文件下载请求
await page.route('/api/export/users.csv', async route => {
const csvContent = 'id,name,email\\n1,John,john@example.com\\n2,Jane,jane@example.com';
await route.fulfill({
status: 200,
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': 'attachment; filename="users.csv"'
},
body: csvContent
});
});
await page.goto('/users');
// 监听下载事件
const downloadPromise = page.waitForEvent('download');
await page.click('[data-testid="export-button"]');
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('users.csv');
// 验证下载内容
const path = await download.path();
const fs = require('fs');
const content = fs.readFileSync(path, 'utf8');
expect(content).toContain('John,john@example.com');
});
});
视觉回归测试
javascript
// tests/visual-regression.spec.js
import { test, expect } from '@playwright/test';
test.describe('视觉回归测试', () => {
test('首页视觉对比', async ({ page }) => {
await page.goto('/');
// 等待页面完全加载
await page.waitForLoadState('networkidle');
// 全页面截图对比
await expect(page).toHaveScreenshot('homepage.png');
});
test('登录表单视觉对比', async ({ page }) => {
await page.goto('/login');
// 对比登录表单
await expect(page.locator('[data-testid="login-form"]'))
.toHaveScreenshot('login-form.png');
});
test('响应式设计视觉测试', async ({ page }) => {
await page.goto('/');
// 桌面视图
await page.setViewportSize({ width: 1920, height: 1080 });
await expect(page).toHaveScreenshot('homepage-desktop.png');
// 平板视图
await page.setViewportSize({ width: 768, height: 1024 });
await expect(page).toHaveScreenshot('homepage-tablet.png');
// 移动端视图
await page.setViewportSize({ width: 375, height: 667 });
await expect(page).toHaveScreenshot('homepage-mobile.png');
});
test('主题切换视觉测试', async ({ page }) => {
await page.goto('/');
// 默认主题
await expect(page).toHaveScreenshot('theme-default.png');
// 切换到暗色主题
await page.click('[data-testid="theme-toggle"]');
await page.waitForTimeout(500); // 等待主题切换动画
await expect(page).toHaveScreenshot('theme-dark.png');
});
test('组件状态视觉测试', async ({ page }) => {
await page.goto('/components');
const button = page.locator('[data-testid="primary-button"]');
// 默认状态
await expect(button).toHaveScreenshot('button-default.png');
// 悬停状态
await button.hover();
await expect(button).toHaveScreenshot('button-hover.png');
// 禁用状态
await page.click('[data-testid="disable-button"]');
await expect(button).toHaveScreenshot('button-disabled.png');
});
});
🚀 性能和调试
性能监控
javascript
// tests/performance.spec.js
import { test, expect } from '@playwright/test';
test.describe('性能测试', () => {
test('页面加载性能', async ({ page }) => {
// 开始性能监控
await page.goto('/dashboard');
// 获取性能指标
const metrics = await page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0];
const paint = performance.getEntriesByType('paint');
return {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime,
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime,
domNodes: document.querySelectorAll('*').length,
resourceCount: performance.getEntriesByType('resource').length
};
});
console.log('Performance Metrics:', metrics);
// 设置性能阈值
expect(metrics.domContentLoaded).toBeLessThan(2000); // DOMContentLoaded < 2s
expect(metrics.firstContentfulPaint).toBeLessThan(1500); // FCP < 1.5s
expect(metrics.domNodes).toBeLessThan(2000); // DOM节点数 < 2000
});
test('资源加载优化验证', async ({ page }) => {
const resources = [];
// 监听资源加载
page.on('response', response => {
resources.push({
url: response.url(),
status: response.status(),
contentType: response.headers()['content-type'],
size: response.headers()['content-length']
});
});
await page.goto('/');
await page.waitForLoadState('networkidle');
// 分析资源
const images = resources.filter(r => r.contentType?.startsWith('image/'));
const scripts = resources.filter(r => r.contentType?.includes('javascript'));
const styles = resources.filter(r => r.contentType?.includes('css'));
console.log(`Images: ${images.length}, Scripts: ${scripts.length}, Styles: ${styles.length}`);
// 验证资源优化
expect(images.length).toBeLessThan(20); // 图片数量限制
expect(scripts.length).toBeLessThan(10); // JS文件数量限制
expect(styles.length).toBeLessThan(5); // CSS文件数量限制
// 验证没有404资源
const notFoundResources = resources.filter(r => r.status === 404);
expect(notFoundResources).toHaveLength(0);
});
});
调试工具
javascript
// tests/debugging.spec.js
import { test, expect } from '@playwright/test';
test.describe('调试功能', () => {
test('详细调试信息', async ({ page }) => {
// 启用详细日志
page.on('console', msg => {
console.log(`[${msg.type()}] ${msg.text()}`);
});
page.on('pageerror', error => {
console.error(`Page Error: ${error.message}`);
});
page.on('requestfailed', request => {
console.error(`Request Failed: ${request.url()} - ${request.failure().errorText}`);
});
await page.goto('/');
// 在调试时暂停
// await page.pause(); // 这会在有头模式下暂停执行
// 执行自定义JavaScript进行调试
const debugInfo = await page.evaluate(() => {
return {
url: window.location.href,
userAgent: navigator.userAgent,
cookies: document.cookie,
localStorage: { ...localStorage },
sessionStorage: { ...sessionStorage }
};
});
console.log('Debug Info:', debugInfo);
});
test('截图调试', async ({ page }) => {
await page.goto('/complex-page');
// 测试步骤间截图
await page.screenshot({
path: 'debug-screenshots/step-1-initial.png',
fullPage: true
});
await page.click('[data-testid="toggle-menu"]');
await page.screenshot({
path: 'debug-screenshots/step-2-menu-open.png',
fullPage: true
});
await page.fill('[data-testid="search-input"]', 'test query');
await page.screenshot({
path: 'debug-screenshots/step-3-search-filled.png',
fullPage: true
});
await page.click('[data-testid="search-button"]');
await page.waitForSelector('[data-testid="search-results"]');
await page.screenshot({
path: 'debug-screenshots/step-4-results.png',
fullPage: true
});
});
test('元素状态调试', async ({ page }) => {
await page.goto('/interactive-page');
const button = page.locator('[data-testid="submit-button"]');
// 调试元素状态
const elementInfo = await button.evaluate(el => ({
tagName: el.tagName,
className: el.className,
id: el.id,
disabled: el.disabled,
style: window.getComputedStyle(el).display,
boundingBox: el.getBoundingClientRect(),
innerHTML: el.innerHTML
}));
console.log('Element Info:', elementInfo);
// 等待元素状态变化
await expect(button).toBeEnabled();
await expect(button).toBeVisible();
});
});
📝 Playwright最佳实践
测试组织和维护
javascript
const PlaywrightBestPractices = {
TEST_ORGANIZATION: {
structure: [
'按功能模块组织测试文件',
'使用页面对象模式封装页面交互',
'共享工具函数和测试数据',
'明确的测试命名约定'
],
maintainability: [
'定期更新选择器和页面对象',
'保持测试的独立性',
'避免硬编码等待时间',
'使用数据驱动测试减少重复'
]
},
STABILITY_PRACTICES: {
waitingStrategies: [
'使用waitForSelector等待元素',
'使用waitForLoadState等待页面状态',
'使用expect的内置等待机制',
'避免使用固定的sleep'
],
selectorStrategies: [
'优先使用data-testid属性',
'避免依赖CSS类名和样式',
'使用语义化的选择器',
'建立选择器的更新机制'
]
},
PERFORMANCE_OPTIMIZATION: {
execution: [
'并行执行独立的测试',
'使用浏览器上下文复用',
'合理配置超时时间',
'优化测试数据的准备'
],
debugging: [
'启用调试模式进行问题排查',
'使用截图和视频记录',
'监控测试执行时间',
'分析失败模式'
]
}
};
📝 总结
Playwright是功能强大的现代E2E测试框架:
- 多浏览器支持:Chromium、Firefox、Safari全覆盖
- API丰富:页面交互、网络拦截、性能监控
- 调试友好:截图、录制、调试模式
- CI/CD集成:容器化部署、并行执行
通过Playwright可以构建稳定、高效的端到端测试体系,确保Web应用的质量和用户体验。