name: playwright-testing description: 当用户询问 “Playwright”、“响应式测试”、“使用 Playwright 测试”、“测试登录流程”、“文件上传测试”、“处理测试中的认证” 或 “修复不稳定测试” 时,应使用此技能。
Playwright 测试最佳实践
测试组织
文件结构
tests/
├── auth/
│ ├── login.spec.ts
│ └── signup.spec.ts
├── dashboard/
│ └── dashboard.spec.ts
├── fixtures/
│ └── test-data.ts
├── pages/
│ └── login.page.ts
└── playwright.config.ts
命名约定
- 文件:
功能名称.spec.ts - 测试: 描述用户行为,而非实现细节
- 好例子:
test('用户可以通过邮箱重置密码') - 坏例子:
test('测试重置密码')
页面对象模型
基本模式
// pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.page.getByLabel("邮箱").fill(email);
await this.page.getByLabel("密码").fill(password);
await this.page.getByRole("button", { name: "登录" }).click();
}
}
// tests/login.spec.ts
test("成功登录", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("user@example.com", "password");
await expect(page).toHaveURL("/dashboard");
});
定位器策略
优先级顺序(从优到劣)
getByRole- 可访问性强,健壮getByLabel- 表单输入getByPlaceholder- 无标签时使用getByText- 可见文本getByTestId- 无更好选项时使用- CSS/XPath - 最后手段
示例
// 首选
await page.getByRole("button", { name: "提交" }).click();
await page.getByLabel("邮箱地址").fill("user@example.com");
// 可接受
await page.getByTestId("submit-button").click();
// 避免
await page.locator("#submit-btn").click();
await page.locator('//button[@type="submit"]').click();
认证处理
存储状态(推荐)
保存登录状态并在测试间重用:
// global-setup.ts
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("/login");
await page.getByLabel("邮箱").fill(process.env.TEST_USER_EMAIL);
await page.getByLabel("密码").fill(process.env.TEST_USER_PASSWORD);
await page.getByRole("button", { name: "登录" }).click();
await page.waitForURL("/dashboard");
await page.context().storageState({ path: "auth.json" });
await browser.close();
}
// playwright.config.ts
export default defineConfig({
globalSetup: "./global-setup.ts",
use: {
storageState: "auth.json",
},
});
多用户场景
// 创建不同认证状态
const adminAuth = "admin-auth.json";
const userAuth = "user-auth.json";
test.describe("管理员功能", () => {
test.use({ storageState: adminAuth });
// 管理员测试
});
test.describe("用户功能", () => {
test.use({ storageState: userAuth });
// 用户测试
});
文件上传处理
基本上传
// 单个文件
await page.getByLabel("上传文件").setInputFiles("path/to/file.pdf");
// 多个文件
await page
.getByLabel("上传文件")
.setInputFiles(["path/to/file1.pdf", "path/to/file2.pdf"]);
// 清除文件输入
await page.getByLabel("上传文件").setInputFiles([]);
拖放上传
// 从缓冲区创建文件
const buffer = Buffer.from("文件内容");
await page.getByTestId("dropzone").dispatchEvent("drop", {
dataTransfer: {
files: [{ name: "test.txt", mimeType: "text/plain", buffer }],
},
});
文件下载
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "下载" }).click();
const download = await downloadPromise;
await download.saveAs("downloads/" + download.suggestedFilename());
等待策略
自动等待(首选)
Playwright 自动等待元素。使用断言:
// 自动等待元素可见且稳定
await page.getByRole("button", { name: "提交" }).click();
// 自动等待条件
await expect(page.getByText("成功")).toBeVisible();
显式等待(需要时)
// 等待导航
await page.waitForURL("**/dashboard");
// 等待网络空闲
await page.waitForLoadState("networkidle");
// 等待特定响应
await page.waitForResponse((resp) => resp.url().includes("/api/data"));
网络模拟
模拟 API 响应
await page.route("**/api/users", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 1, name: "测试用户" }]),
});
});
// 模拟错误响应
await page.route("**/api/users", async (route) => {
await route.fulfill({ status: 500 });
});
拦截和修改
await page.route("**/api/data", async (route) => {
const response = await route.fetch();
const json = await response.json();
json.modified = true;
await route.fulfill({ response, json });
});
CI/CD 集成
GitHub Actions 示例
- name: 运行 Playwright 测试
run: npx playwright test
env:
CI: true
- name: 上传测试结果
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
并行执行
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 2 : undefined,
fullyParallel: true,
});
调试失败测试
调试工具
# 使用 UI 模式运行
npx playwright test --ui
# 使用检查器运行
npx playwright test --debug
# 显示浏览器
npx playwright test --headed
跟踪查看器
// playwright.config.ts
use: {
trace: 'on-first-retry', // 失败时捕获跟踪
}
修复不稳定测试
常见原因和解决方案
竞态条件:
- 使用适当断言而非硬等待
- 等待网络请求完成
动画问题:
- 在测试配置中禁用动画
- 等待动画完成
动态内容:
- 使用灵活定位器(文本内容而非位置)
- 等待加载状态解析
测试隔离:
- 每个测试应设置自身状态
- 不依赖其他测试的副作用
应避免的反模式
// 坏: 硬等待
await page.waitForTimeout(5000);
// 好: 等待条件
await expect(page.getByText("已加载")).toBeVisible();
// 坏: 不稳定选择器
await page.locator(".btn:nth-child(3)").click();
// 好: 语义选择器
await page.getByRole("button", { name: "提交" }).click();
响应式设计测试
要进行全面的响应式设计测试,可使用 responsive-tester 代理。它自动:
- 在 7 个标准断点(375px 至 1536px)测试页面
- 检测水平溢出问题
- 验证移动优先设计模式
- 检查触摸目标大小(最小 44x44px)
- 标记反模式,如无移动后备的固定宽度
通过询问 “测试响应式”、“检查断点” 或 “测试移动/桌面布局” 来触发。