skillhub-147-playwright-skill
502 行
17 KiB
Markdown
502 行
17 KiB
Markdown
# 报告与构建产物
|
||
|
||
> **使用时机**:配置测试输出,用于本地调试、CI 仪表盘与团队可视性。每个项目从第一天起就需要一个报告策略。
|
||
|
||
## 快速参考
|
||
|
||
```bash
|
||
# 查看最新 HTML 报告
|
||
npx playwright show-report
|
||
|
||
# 使用特定报告器运行
|
||
npx playwright test --reporter=html
|
||
npx playwright test --reporter=dot # 最简 CI 输出
|
||
npx playwright test --reporter=line # 每个测试一行
|
||
npx playwright test --reporter=json # 机器可读
|
||
npx playwright test --reporter=junit # CI 集成
|
||
|
||
# 通过 CLI 使用多个报告器
|
||
npx playwright test --reporter=dot,html
|
||
|
||
# 合并分片报告
|
||
npx playwright merge-reports --reporter=html ./blob-report
|
||
```
|
||
|
||
## 模式
|
||
|
||
### 模式 1:多报告器配置
|
||
|
||
**使用时机**:每个项目都应如此。你始终需要至少两个报告器:一个给人看,一个给 CI 用。
|
||
**避免时机**:永远不需要——始终配置报告器。
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
reporter: process.env.CI
|
||
? [
|
||
// CI:机器可读 + 人类可读 + CI 注释
|
||
["dot"], // 最简控制台输出
|
||
["html", { open: "never" }], // 可浏览的报告(作为构建产物上传)
|
||
["junit", { outputFile: "test-results/junit.xml" }], // CI 测试标签页集成
|
||
["github"], // PR 注释(仅限 GitHub Actions)
|
||
]
|
||
: [
|
||
// 本地:详细控制台 + 自动打开报告
|
||
["list"], // 详细控制台输出
|
||
["html", { open: "on-failure" }], // 失败时自动打开
|
||
],
|
||
})
|
||
```
|
||
|
||
### 模式 2:内置报告器详解
|
||
|
||
**使用时机**:为你的场景选择正确的报告器。
|
||
|
||
| 报告器 | 输出 | 最佳用途 |
|
||
| ---------- | --------------------------------- | --------------------------- |
|
||
| `list` | 每个测试一行,显示通过/失败 | 本地开发 |
|
||
| `line` | 测试完成时更新单行 | 本地,较简洁 |
|
||
| `dot` | 每个测试一个点:`.` 通过,`F` 失败 | CI 日志(最简) |
|
||
| `html` | 带有追踪记录的交互式 HTML 页面 | 运行后分析 |
|
||
| `json` | 机器可读的 JSON 输出到 stdout 或文件 | 自定义工具、仪表盘 |
|
||
| `junit` | JUnit XML | CI 平台(Azure DevOps、Jenkins、CircleCI) |
|
||
| `github` | GitHub Actions 注释 | GitHub PR |
|
||
| `blob` | 用于分片合并的二进制归档 | 分片式 CI 运行 |
|
||
|
||
**JSON 报告器——写入文件:**
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
reporter: [["json", { outputFile: "test-results/results.json" }]],
|
||
})
|
||
```
|
||
|
||
**JUnit 报告器——自定义输出:**
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
reporter: [
|
||
[
|
||
"junit",
|
||
{
|
||
outputFile: "test-results/junit.xml",
|
||
stripANSIControlSequences: true,
|
||
includeProjectInTestName: true,
|
||
},
|
||
],
|
||
],
|
||
})
|
||
```
|
||
|
||
### 模式 3:自定义报告器
|
||
|
||
**使用时机**:内置报告器不满足你的需求——你需要 Slack 通知、数据库日志或自定义仪表盘。
|
||
**避免时机**:内置报告器或现有的第三方报告器已能满足你的情况。
|
||
|
||
```ts
|
||
// reporters/slack-reporter.ts
|
||
import type {
|
||
FullConfig,
|
||
FullResult,
|
||
Reporter,
|
||
Suite,
|
||
TestCase,
|
||
TestResult,
|
||
} from "@playwright/test/reporter"
|
||
|
||
class SlackReporter implements Reporter {
|
||
private passed = 0
|
||
private failed = 0
|
||
private skipped = 0
|
||
private failures: string[] = []
|
||
|
||
onTestEnd(test: TestCase, result: TestResult) {
|
||
switch (result.status) {
|
||
case "passed":
|
||
this.passed++
|
||
break
|
||
case "failed":
|
||
case "timedOut":
|
||
this.failed++
|
||
this.failures.push(`${test.title}: ${result.error?.message?.split("\n")[0]}`)
|
||
break
|
||
case "skipped":
|
||
this.skipped++
|
||
break
|
||
}
|
||
}
|
||
|
||
async onEnd(result: FullResult) {
|
||
const total = this.passed + this.failed + this.skipped
|
||
const emoji = this.failed > 0 ? ":red_circle:" : ":large_green_circle:"
|
||
const text = [
|
||
`${emoji} *Playwright Tests*: ${result.status}`,
|
||
`Passed: ${this.passed} | Failed: ${this.failed} | Skipped: ${this.skipped} | Total: ${total}`,
|
||
`Duration: ${(result.duration / 1000).toFixed(1)}s`,
|
||
]
|
||
|
||
if (this.failures.length > 0) {
|
||
text.push("", "*Failures:*")
|
||
this.failures.slice(0, 5).forEach((f) => text.push(` - ${f}`))
|
||
if (this.failures.length > 5) {
|
||
text.push(` ...and ${this.failures.length - 5} more`)
|
||
}
|
||
}
|
||
|
||
const webhookUrl = process.env.SLACK_WEBHOOK_URL
|
||
if (webhookUrl) {
|
||
await fetch(webhookUrl, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ text: text.join("\n") }),
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
export default SlackReporter
|
||
```
|
||
|
||
**注册自定义报告器:**
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
reporter: [["dot"], ["html", { open: "never" }], ["./reporters/slack-reporter.ts"]],
|
||
})
|
||
```
|
||
|
||
### 模式 4:追踪记录文件管理
|
||
|
||
**使用时机**:调试测试失败时。追踪记录会捕获操作、网络请求、DOM 快照和控制台日志的完整时间线。
|
||
**避免时机**:永远不要在 CI 中完全禁用追踪记录——`on-first-retry` 设置的额外开销极小。
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
retries: process.env.CI ? 2 : 0,
|
||
use: {
|
||
// 'on-first-retry':仅在测试失败并重试时记录追踪。
|
||
// 通过的测试几乎没有额外开销,失败时则可完整调试。
|
||
trace: "on-first-retry",
|
||
},
|
||
})
|
||
```
|
||
|
||
**追踪选项:**
|
||
|
||
| 值 | 记录追踪 | 时机 | 额外开销 |
|
||
| --------------------------- | ------------------------------- | -------------- | --------------------- |
|
||
| `'off'` | 从不 | -- | 无 |
|
||
| `'on'` | 每个测试 | 始终 | 高(文件体积大) |
|
||
| `'on-first-retry'` | 失败后首次重试时 | 仅重试时 | 极小 |
|
||
| `'retain-on-failure'` | 每个测试,只保留失败的 | 失败时 | 中等 |
|
||
| `'retain-on-first-failure'` | 每个测试,只保留首次失败 | 首次失败时 | 中等 |
|
||
|
||
**查看追踪记录:**
|
||
|
||
```bash
|
||
# 本地打开追踪查看器
|
||
npx playwright show-trace test-results/my-test/trace.zip
|
||
|
||
# 从 HTML 报告打开追踪(在报告中点击"Traces"标签页)
|
||
npx playwright show-report
|
||
|
||
# 在线追踪查看器(上传 trace.zip)
|
||
# https://trace.playwright.dev
|
||
```
|
||
|
||
### 模式 5:截图与视频配置
|
||
|
||
**使用时机**:测试失败的视觉证据对调试或错误报告很有价值。
|
||
**避免时机**:永远不要在 CI 中禁用截图——`on-failure` 设置成本很低。
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
use: {
|
||
// 截图
|
||
screenshot: "only-on-failure", // 失败时捕获最终状态
|
||
|
||
// 视频
|
||
video: "retain-on-failure", // 全部录制,只保留失败的
|
||
|
||
// 视频尺寸(可选——越小越省磁盘)
|
||
video: {
|
||
mode: "retain-on-failure",
|
||
size: { width: 1280, height: 720 },
|
||
},
|
||
},
|
||
})
|
||
```
|
||
|
||
**截图选项:**
|
||
|
||
| 值 | 捕获时机 | 磁盘开销 |
|
||
| -------------------- | ------------------- | --------- |
|
||
| `'off'` | 从不 | 无 |
|
||
| `'on'` | 每个测试(结束时) | 高 |
|
||
| `'only-on-failure'` | 仅失败的测试 | 低 |
|
||
|
||
**视频选项:**
|
||
|
||
| 值 | 录制 | 保留 | 磁盘开销 |
|
||
| ---------------------- | --------- | -------------- | --------- |
|
||
| `'off'` | 从不 | -- | 无 |
|
||
| `'on'` | 每个测试 | 全部 | 非常高 |
|
||
| `'on-first-retry'` | 重试时 | 重试过的测试 | 低 |
|
||
| `'retain-on-failure'` | 每个测试 | 仅失败的 | 中等 |
|
||
|
||
### 模式 6:CI 构建产物组织
|
||
|
||
**使用时机**:在 CI 中保持测试构建产物有序且可访问。
|
||
|
||
**推荐的目录结构:**
|
||
|
||
```
|
||
test-results/ # Playwright 默认输出目录
|
||
├── my-test-chromium/
|
||
│ ├── trace.zip # 追踪文件
|
||
│ ├── test-failed-1.png # 截图
|
||
│ └── video.webm # 视频录制
|
||
├── another-test-firefox/
|
||
│ ├── trace.zip
|
||
│ └── test-failed-1.png
|
||
└── junit.xml # JUnit 报告(如已配置)
|
||
|
||
playwright-report/ # HTML 报告目录
|
||
├── index.html
|
||
└── data/
|
||
└── ...
|
||
|
||
blob-report/ # 用于分片合并的 blob 报告
|
||
└── report-1.zip
|
||
```
|
||
|
||
**GitHub Actions 构建产物上传:**
|
||
|
||
```yaml
|
||
# 上传 HTML 报告(始终上传——即使测试通过也有用)
|
||
- uses: actions/upload-artifact@v4
|
||
if: ${{ !cancelled() }}
|
||
with:
|
||
name: playwright-report
|
||
path: playwright-report/
|
||
retention-days: 14
|
||
|
||
# 上传追踪记录和截图(仅失败时——节省存储空间)
|
||
- uses: actions/upload-artifact@v4
|
||
if: failure()
|
||
with:
|
||
name: test-traces
|
||
path: |
|
||
test-results/**/trace.zip
|
||
test-results/**/*.png
|
||
test-results/**/*.webm
|
||
retention-days: 7
|
||
```
|
||
|
||
### 模式 7:Allure 集成
|
||
|
||
**使用时机**:你的团队在多个测试框架中使用 Allure 进行测试报告。
|
||
**避免时机**:内置的 HTML 报告器已能满足你的需求(通常确实如此)。
|
||
|
||
```bash
|
||
# 安装 Allure 报告器
|
||
npm install -D allure-playwright
|
||
```
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
reporter: [
|
||
["line"],
|
||
[
|
||
"allure-playwright",
|
||
{
|
||
detail: true,
|
||
outputFolder: "allure-results",
|
||
suiteTitle: true,
|
||
},
|
||
],
|
||
],
|
||
})
|
||
```
|
||
|
||
```bash
|
||
# 生成并查看 Allure 报告
|
||
npx allure generate allure-results -o allure-report --clean
|
||
npx allure open allure-report
|
||
|
||
# 或使用 Allure CLI
|
||
allure serve allure-results
|
||
```
|
||
|
||
**为测试添加 Allure 元数据:**
|
||
|
||
```ts
|
||
import { test, expect } from "@playwright/test"
|
||
import { allure } from "allure-playwright"
|
||
|
||
test("checkout flow", async ({ page }) => {
|
||
await allure.epic("E-Commerce")
|
||
await allure.feature("Checkout")
|
||
await allure.story("Credit Card Payment")
|
||
await allure.severity("critical")
|
||
|
||
await page.goto("/checkout")
|
||
// ... 测试实现
|
||
})
|
||
```
|
||
|
||
## 决策指南
|
||
|
||
| 场景 | 报告器配置 | 原因 |
|
||
| ------------------- | -------------------------------------------------- | --------------------------------------------------- |
|
||
| 本地开发 | `[['list'], ['html', { open: 'on-failure' }]]` | 详细控制台 + 失败时自动打开报告 |
|
||
| GitHub Actions | `[['dot'], ['html'], ['github']]` | 最简日志 + 报告构建产物 + PR 注释 |
|
||
| GitLab CI | `[['dot'], ['html'], ['junit']]` | 最简日志 + 报告构建产物 + 测试标签页 |
|
||
| Azure DevOps/Jenkins | `[['dot'], ['html'], ['junit']]` | JUnit 用于原生测试结果集成 |
|
||
| 分片式 CI | `[['blob'], ['github']]` | Blob 用于合并;github 用于 PR 注释 |
|
||
| 团队使用 Allure | `[['line'], ['allure-playwright']]` | 跨框架报告一致性 |
|
||
| 自定义仪表盘 | `[['json', { outputFile: '...' }]]` + 自定义报告器 | JSON 提供数据,自定义用于通知 |
|
||
|
||
| 构建产物 | 收集时机 | 保留天数 | 上传条件 |
|
||
| -------------------- | -------------- | -------- | ----------------------------- |
|
||
| HTML 报告 | 始终 | 14 天 | `if: ${{ !cancelled() }}` |
|
||
| 追踪记录 (`.zip`) | 失败时 | 7 天 | `if: failure()` |
|
||
| 截图 (`.png`) | 失败时 | 7 天 | `if: failure()` |
|
||
| 视频 (`.webm`) | 失败时 | 7 天 | `if: failure()` |
|
||
| JUnit XML | 始终 | 14 天 | `if: ${{ !cancelled() }}` |
|
||
| Blob 报告 | 始终(分片时) | 1 天 | `if: ${{ !cancelled() }}` |
|
||
|
||
## 反模式
|
||
|
||
| 反模式 | 问题 | 应改为 |
|
||
| ---------------------------------- | -------------------------------------------------- | --------------------------------------------------------------- |
|
||
| 未配置报告器 | 仅默认 `list`;无可持久化的报告 | 始终配置 `html` + 一个 CI 报告器 |
|
||
| 在 CI 中使用 `trace: 'on'` | 巨大的构建产物(每个测试 50-100 MB),上传缓慢 | 使用 `trace: 'on-first-retry'` |
|
||
| 在 CI 中使用 `video: 'on'` | 存储成本极高;拖慢测试执行速度 | 使用 `video: 'retain-on-failure'` |
|
||
| 仅在失败时上传构建产物 | 测试通过时没有报告;无法验证结果 | 使用 `if: ${{ !cancelled() }}`(始终)上传 |
|
||
| 构建产物无保留期限 | CI 存储空间在数周内被填满 | 设置 `retention-days: 7-14` |
|
||
| 仅使用 `dot` 报告器,无 HTML | 运行后无法深入查看失败的测试 | 在 CI 中始终将 `dot` 与 `html` 配对使用 |
|
||
| JUnit 输出到 stdout | 干扰控制台输出;难以解析 | 写入文件:`['junit', { outputFile: 'results/junit.xml' }]` |
|
||
| 自定义报告器阻塞 `onEnd` | 缓慢的 Slack/HTTP 调用延迟流水线完成 | 在自定义报告器中使用 `Promise.race` 加超时 |
|
||
|
||
## 故障排除
|
||
|
||
### HTML 报告为空或缺少测试
|
||
|
||
**原因**:另一个报告器存在冲突,或者 `outputFolder` 被覆盖为非默认路径。
|
||
|
||
**修复**:检查你的报告器配置。HTML 报告默认输出到 `playwright-report/`:
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
reporter: [["html", { outputFolder: "playwright-report", open: "never" }]],
|
||
})
|
||
```
|
||
|
||
### 追踪记录过大,无法作为 CI 构建产物上传
|
||
|
||
**原因**:`trace: 'on'` 录制了每个测试,包括通过的。
|
||
|
||
**修复**:切换为 `'on-first-retry'`,并确保在 CI 中 `retries > 0`:
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
retries: process.env.CI ? 2 : 0,
|
||
use: {
|
||
trace: "on-first-retry",
|
||
},
|
||
})
|
||
```
|
||
|
||
### JUnit XML 未被 CI 平台识别
|
||
|
||
**原因**:输出路径与 CI 任务期望的路径不匹配,或者文件为空。
|
||
|
||
**修复**:确保路径与你的 CI 配置一致:
|
||
|
||
```ts
|
||
// playwright.config.ts -- outputFile 路径
|
||
reporter: [['junit', { outputFile: 'test-results/junit.xml' }]],
|
||
```
|
||
|
||
```yaml
|
||
# GitHub Actions
|
||
- uses: dorny/test-reporter@v1
|
||
with:
|
||
path: test-results/junit.xml
|
||
reporter: java-junit
|
||
|
||
# Azure DevOps
|
||
- task: PublishTestResults@2
|
||
inputs:
|
||
testResultsFiles: 'test-results/junit.xml'
|
||
|
||
# Jenkins
|
||
junit 'test-results/junit.xml'
|
||
```
|
||
|
||
### `merge-reports` 生成空报告
|
||
|
||
**原因**:分片使用了 `html` 报告器而非 `blob`。只有 `blob` 输出可以被合并。
|
||
|
||
**修复**:对分片运行使用 blob 报告器:
|
||
|
||
```ts
|
||
// playwright.config.ts
|
||
import { defineConfig } from "@playwright/test"
|
||
|
||
export default defineConfig({
|
||
reporter: process.env.CI
|
||
? [["blob"], ["dot"]] // blob 用于合并,dot 用于控制台
|
||
: [["html", { open: "on-failure" }]],
|
||
})
|
||
```
|
||
|
||
### 截图中未出现在 HTML 报告中
|
||
|
||
**原因**:`screenshot: 'off'`,或者截图在 `test-results/` 中但未链接到报告。
|
||
|
||
**修复**:启用截图并确保两个目录都可用:
|
||
|
||
```ts
|
||
use: {
|
||
screenshot: 'only-on-failure',
|
||
},
|
||
```
|
||
|
||
HTML 报告会自动嵌入来自 `test-results/` 的截图。如果你移动或删除了 `test-results/`,截图将从报告中丢失。
|
||
|
||
## 相关文档
|
||
|
||
- [ci/ci-github-actions.md](ci-github-actions.md) —— GitHub Actions 中的构建产物上传
|
||
- [ci/ci-gitlab.md](ci-gitlab.md) —— GitLab 中的构建产物配置
|
||
- [ci/parallel-and-sharding.md](parallel-and-sharding.md) —— 用于分片运行的 blob 报告器
|
||
- [core/configuration.md](../core/configuration.md) —— trace、screenshot、video 设置
|
||
- [core/debugging.md](../core/debugging.md) —— 使用追踪记录和截图进行调试
|