关于 playwright-test-results
playwright-test-results 是面向 Playwright CI 结果分析的技能,将近期测试结果存入单个 DuckDB 文件,供 SQL 查询失败、波动和耗时情况。数据库会定期更新,并保留近期运行摘要。
主要功能
- 01DuckDB 保存近期测试结果
- 02按重试记录每次结果
- 03SQL 分析失败与波动
- 04按标签筛选测试
- 05生成带链接的运行历史
适用场景
- 01定位跨运行结果不稳定的测试
- 02按耗时或失败情况排序测试
- 03筛选带有 @slow 等标签的测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ playwright --skill playwright-test-results安装后在 Claude Code 中输入/playwright-test-results确认已加载。
详细介绍
Playwright 测试结果(DuckDB)
一个 DuckDB 文件保存近期 Playwright CI 测试结果,因此你可以通过普通 SQL 查询失败、不稳定性和运行缓慢的测试。它每隔几小时刷新一次。
获取数据库
下载最新快照,并补充最新的运行结果:
npm ci # first time only, from the repo root
GITHUB_TOKEN=$(gh auth token) node utils/test-results-db/cli.ts update --lookback-days 3
如果本地没有数据库,update 会先下载快照,然后只导入快照中缺少的制品。切勿删除数据库来重新构建:每个制品都会消耗一次 GitHub API 请求,所有工作流共享仓库的请求额度,而且重建几天的数据就会耗尽额度。若只想刷新快照而不导入数据,请运行 download。
通过捆绑的 @duckdb/node-api 绑定查询数据库——无需单独安装 DuckDB;运行 npm ci 后,它会随 node_modules 一并安装:
node --input-type=module -e '
import { DuckDBInstance } from "@duckdb/node-api";
const conn = await (await DuckDBInstance.create("utils/test-results-db/test-results.duckdb")).connect();
console.table((await conn.runAndReadAll(process.argv[1])).getRowObjectsJson());
' "SELECT count(*) FROM test_results"
整数列会以字符串形式返回(确保 JSON 安全),因此请在 SQL 中进行排序和筛选,而不要在 JS 中处理。
架构
单个 test_results 表,每条测试结果占一行(每次重试占一行)。列根据 reporter 输出的 parquet 推断得出(tests/config/parquetReporter.ts),此外 CLI 还会添加两个末尾列:
| 列 | 含义 |
|---|---|
run_id, run_attempt | GitHub Actions 运行标识 |
run_started_at | 运行开始时间 |
workflow_name | 例如 tests 1 / tests 2 / tests others / MCP |
event | push / pull_request |
head_sha, head_branch, pr_number | 被测试的内容 |
bot_name | 例如 chromium-ubuntu-22.04-node20、webkit-macos-15-large——CI 机器人。操作系统和架构编码在此列中;没有单独的操作系统列。 |
project_name | CI 项目 = 浏览器 + 测试套件,例如 chromium-page、webkit-library、playwright-test |
test_title | 文件内的标题路径,以 › 连接(describe › test) |
file, line, column_number | 源代码位置(文件路径相对于仓库根目录) |
expected_status | passed / skipped / ... |
status | 实际结果:passed / failed / timedOut / skipped / interrupted |
retry | 0 = 首次尝试 |
result_started_at | 本次尝试开始时间 |
duration_ms | 结果耗时 |
error_message | 合并所有错误并移除 ANSI 转义序列(没有错误时为 NULL) |
tags | 字符串列表,例如 ['@slow', '@flaky'](使用列表函数 / list_contains) |
annotations | {type, description} 结构体列表,例如 [{'type': 'skip', 'description': 'flaky on CI'}](没有注释时为空列表) |
artifact_id | 此行对应的 GitHub 制品(去重键) |
ingested_at | 仅用于调试——此行的导入时间 |
说明:
- 测试由
(project_name, file, test_title)标识——按此元组分组。(Playwright 的test_id哈希不会存储;这三列是其哈希前的原始数据。) - 不稳定性是推导得出的,而非直接存储。最重要的信号是跨运行情况:测试在重试后的最终结果会在不同运行之间变化——有时通过,有时失败。另一种单独的单次运行内不稳定,指测试在一次运行中经重试后恢复通过(
failed→passed)。 - **真实失败与有意失败:**筛选
expected_status = 'passed'。标记为test.fail()的测试会以expected_status='failed'记录为status='failed';否则,这些测试会占据任何“失败次数最多”列表的主要位置。 - 数据库按运行次数限制大小:随着时间推移,最早的完整运行会被移除,因此数据库只保留近期时间窗口,而非完整历史记录。
查询示例
按 (project_name, file, test_title) 对测试分组;在查询失败或不稳定性时,将范围限定为 expected_status = 'passed',以免有意失败的 test.fail() 测试扭曲结果。
跨运行不稳定——测试的最终结果会在不同运行之间变化(这会让 CI 运行失败的原因变得模糊)。least(failed_runs, passed_runs) 会将真正呈双峰分布的测试排在始终损坏或偶发失败的测试之前:
WITH per_run AS (
SELECT project_name, file, test_title, run_id, run_attempt,
arg_max(status, retry) AS final_status,
any_value(expected_status) AS expected
FROM test_results
GROUP BY project_name, file, test_title, run_id, run_attempt)
SELECT project_name, test_title,
count(*) AS runs,
count(*) FILTER (WHERE final_status IN ('failed','timedOut')) AS failed_runs,
count(*) FILTER (WHERE final_status = 'passed') AS passed_runs,
round(100.0 * count(*) FILTER (WHERE final_status IN ('failed','timedOut'))
/ count(*), 1) AS fail_pct
FROM per_run
WHERE expected = 'passed'
GROUP BY project_name, test_title
HAVING failed_runs > 0 AND passed_runs > 0 AND runs >= 10
ORDER BY least(failed_runs, passed_runs) DESC, failed_runs DESC
LIMIT 20;
按标签筛选(tags 是列表,而非字符串):
SELECT project_name, test_title, count(*) AS runs
FROM test_results
WHERE list_contains(tags, '@slow')
GROUP BY project_name, test_title
ORDER BY runs DESC
LIMIT 20;
生成带链接的表情符号运行历史
要生成紧凑的结果,直接贴到 GitHub 评论中,请将每次最终运行判定渲染为带链接的方块。编辑四个测试标识字段,然后运行:
node --input-type=module <<'EOF'
import { DuckDBInstance } from "@duckdb/node-api";
const repository = "microsoft/playwright";
const test = {
projectName: "firefox-library",
file: "library/proxy.spec.ts",
testTitle: "should exclude patterns",
botName: "firefox-macos-15-large",
};
const conn = await (await DuckDBInstance.create(
"utils/test-results-db/test-results.duckdb"
)).connect();
const result = await conn.runAndReadAll(`
WITH per_run AS (
SELECT run_id, run_attempt,
any_value(run_started_at) AS run_started_at,
arg_max(status, retry) AS final_status,
arg_max(expected_status, retry) AS expected_status,
list(status ORDER BY retry) AS attempt_statuses
FROM test_results
WHERE project_name = $projectName
AND file = $file
AND test_title = $testTitle
AND bot_name = $botName
GROUP BY run_id, run_attempt
)
SELECT run_id, run_attempt, final_status, attempt_statuses
FROM per_run
WHERE expected_status = 'passed'
AND final_status IN ('passed', 'failed', 'timedOut')
ORDER BY run_started_at, run_id, run_attempt
`, test);
const markdown = result.getRowObjectsJson().map(row => {
const rescued = row.final_status === "passed" &&
row.attempt_statuses.some(status => status === "failed" || status === "timedOut");
const emoji = rescued ? "🟧" : row.final_status === "passed" ? "🟩" : "🟥";
const url = `https://github.com/${repository}/actions/runs/${row.run_id}/attempts/${row.run_attempt}`;
return `[${emoji}](${url})`;
}).join("");
console.log(markdown);
EOF
输出为 Markdown:
[🟩](https://github.com/microsoft/playwright/actions/runs/123/attempts/1)[🟧](https://github.com/microsoft/playwright/actions/runs/456/attempts/1)[🟥](https://github.com/microsoft/playwright/actions/runs/789/attempts/1)
每个方块代表一次工作流运行尝试,按时间从早到晚排列。绿色表示通过,橙色表示重试挽救了之前的失败,红色表示失败或超时。arg_max(status, retry) 会选取重试后的最终判定,而按 (run_id, run_attempt) 分组可避免重试变成额外的方块。/attempts/<n> URL 会链接到产生该结果的确切重跑记录。
获取完整详情
数据库存储每项结果的摘要。若要获取完整的步骤树、附件和标准输入输出,请获取该次运行的原始 blob 报告(如果运行上传了报告)。一行数据通过 run_id 和 bot_name 标识该报告:该次运行的 blob 工件名为 blob-report-<bot_name>。
# List the run's blob artifacts and find the one for this bot_name:
gh api /repos/microsoft/playwright/actions/runs/<run_id>/artifacts \
--jq '.artifacts[] | select(.name | startswith("blob-report")) | {id, name}'
# Download it (name == "blob-report-<bot_name>"):
gh api /repos/microsoft/playwright/actions/artifacts/<artifact_id>/zip > blob.zip
Blob 和 parquet 工件的保留期为 7 天,因此此方法仅适用于近期运行;数据库本身会更长时间地保留摘要(直到因运行数量达到上限而被淘汰)。