@clippboard-health/ playwright-reporter-llm 页面存档备份,存于互联网档案馆 @clippboard-health/ playwright-reporter-llm
Playwright reporter,为LLM代理输出结构化的JSON. 最小控制台输出,平分计划,容易被过滤出故障.
目录
安装
npm install @clipboard-health/playwright-reporter-llm
// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
reporter: [
["@clipboard-health/playwright-reporter-llm", { outputFile: "test-results/llm-report.json" }],
],
});
使用量
选项
| 选项 | 默认 | 说明 |
|---|---|---|
outputFile | test-results/llm-report.json | JSON 输出路径 |
主控台输出
.......F..........F.S....
26 tests | 23 passed | 2 failed | 1 skipped (4.2s)
Report: test-results/llm-report.json
要读什么,按任务
| 任务 | 字段 |
|---|---|
| 出入/故障概览 | summary |
| 曲折失败 | 过滤器 tests[] 以 status === "failed",然后读取 errors[0].{message,diff,location,snippet} |
| 诊断定位器分离 | tests[].errors[0].{apiName,selector,actionLog} 失败 tests[].attempts[].error.{apiName,selector,actionLog} 对于失败的动作的解析定位日志 |
| 识别碎片 | 过滤器 tests[] 以 flaky === true; 比较 attempts[] 状态 |
| 重建故障时间表 | 选一个尝试 status !== "passed" 改为: .timeline[] (步骤+网络+控制台,由 offsetMs(中文(简体) ) |
| 检查失败的请求 | tests[].attempts[].network.instances[] 过滤器通过 status >= 400,或加入 groups[groupId].{failureText,wasAborted} |
| 查找请求正文 | tests[].attempts[].network.bodies[instance.requestBodyRef | instance.responseBodyRef] |
| 关联浏览器和后端生命周期 | tests[].attempts[].network.instances[].clientLifecycle 加号 .traceId 编号 : .spanId 编号 : .requestId 编号 : .correlationId |
| 调试未发现的页面错误 | tests[].attempts[].consoleMessages[] 过滤器通过 type 内 "error" | "pageerror" | "page-crashed" |
| 视觉调试 | tests[].attempts[].failureArtifacts.{screenshotBase64,videoPath} |
最小失败实例
走开 attempts[].timeline[] (步骤+网络+控制台,由 offsetMs)重建失败. 每个时间线网络条目包含一个 networkId 你可以解决 attempts[].network.instances[] 详细情况 attempts[].network.groups[] 编号 : bodies[] 用于共享形状/负载上下文:
{
"schemaVersion": 3,
"summary": {
"total": 10,
"passed": 9,
"failed": 1,
"flaky": 0,
"skipped": 0,
"timedOut": 0,
"interrupted": 0
},
"tests": [
{
"id": "abc123",
"title": "Checkout > applies discount code",
"status": "failed",
"flaky": false,
"location": { "file": "tests/checkout.spec.ts", "line": 42, "column": 5 },
"errors": [
{
"message": "Expected: 90\nReceived: 100",
"diff": { "expected": "90", "actual": "100" },
"location": { "file": "tests/checkout.spec.ts", "line": 58, "column": 7 }
}
],
"attempts": [
{
"attempt": 1,
"status": "failed",
"failureArtifacts": { "screenshotBase64": "iVBORw0KGgo...", "videoPath": "video.webm" },
"timeline": [
{
"kind": "step",
"offsetMs": 1050,
"title": "click Apply",
"category": "test.step",
"durationMs": 40,
"depth": 0
},
{
"kind": "network",
"offsetMs": 1100,
"networkId": "n3",
"method": "POST",
"url": "https://api.example.com/discount",
"status": 500
},
{
"kind": "console",
"offsetMs": 1240,
"type": "pageerror",
"text": "Uncaught TypeError: discount is undefined"
},
{
"kind": "step",
"offsetMs": 1300,
"title": "expect total to equal 90",
"category": "pw:api",
"durationMs": 5,
"depth": 0,
"error": "Expected: 90\nReceived: 100"
}
]
}
]
}
]
}
读取为: 点击“ 应用 ” 后端返回 500 ” 。 决定 networkId: "n3" 反对 attempts[0].network.instances 按请求 traceId 编号 : spanId 编号 : requestId /体推器; groups[instance.groupId] (c) 保持形状和总数(发生率,第一/最后冲抵).
Flaky 测试实例( pass- vs- fault 比较)
对于尝试1失败、尝试2失败的试验,两个时限之间的分歧是诊断。 查找第一个不同条目:
{
"tests": [
{
"title": "Checkout > shows confirmation",
"status": "passed",
"flaky": true,
"attempts": [
{
"attempt": 1,
"status": "failed",
"timeline": [
{
"kind": "step",
"offsetMs": 800,
"title": "goto /checkout",
"category": "test.step",
"durationMs": 120,
"depth": 0
},
{
"kind": "network",
"offsetMs": 950,
"networkId": "n0",
"method": "GET",
"url": "https://api.example.com/cart",
"status": 200
},
{
"kind": "step",
"offsetMs": 1400,
"title": "expect banner visible",
"category": "pw:api",
"durationMs": 5000,
"depth": 0,
"error": "Timeout 5000ms exceeded"
}
]
},
{
"attempt": 2,
"status": "passed",
"timeline": [
{
"kind": "step",
"offsetMs": 800,
"title": "goto /checkout",
"category": "test.step",
"durationMs": 120,
"depth": 0
},
{
"kind": "network",
"offsetMs": 950,
"networkId": "n0",
"method": "GET",
"url": "https://api.example.com/cart",
"status": 200
},
{
"kind": "network",
"offsetMs": 1100,
"networkId": "n1",
"method": "GET",
"url": "https://api.example.com/inventory",
"status": 200
},
{
"kind": "step",
"offsetMs": 1350,
"title": "expect banner visible",
"category": "pw:api",
"durationMs": 40,
"depth": 0
}
]
}
]
}
]
}
分歧:通过的努力增加了 /inventory 称失败的尝试没有。 这要么是僵硬的前端缓存,要么是比赛——不是片状测试,是真正的bug.
完整实例报告
见 docs/example-report.json 对于包含代表性可选字段的完整报告(网络计时、重定向链、头条、附件、步巢、多尝试重试).
外地参考
summary-- 快速通过/失效计数tests[].errors[].message- ANSI- stripped, clean 错误文本tests[].errors[].diff-- 从断言错误中提取的预期/实际tests[].errors[].location-- 准确的文件和失败行tests[].errors[].{apiName,selector,actionLog}编号 :tests[].attempts[].error.{apiName,selector,actionLog}- 无法在可用的跟踪日志中显示 Playwright 动作上下文, 包括在片面测试中失败的尝试 。 用于定位和解离的 DOM 失败; 动作日志消息上限为 512 个字符, 每个错误最多 20 个条目, 并带有省略标记tests[].flaky-- 如果测试在重试后通过, 则为真tests[].attempts[]- 全面重试历史,包括每个试题状态、时间、标准、附件、步骤和网络tests[].attempts[].consoleMessages[]-- 警告/错误/页码/页码/页码/页码微量条目(2KB文本封顶,带有[truncated]标记,每次尝试最多为50个,高信号条目优先于低信号)tests[].steps编号 :tests[].network编号 :tests[].timeline-- 最后尝试中的便利别名tests[].attempts[].timeline[]- 统一 排序offsetMs所有保留事件数组kind: "step" | "network" | "console") (中文(简体) ). 快速时间扫描的闪烁-下行条目;所有细节保留在源数组中offsetMs- 从尝试开始的毫秒startTime。总是在步骤上(从TestStep.startTime) (中文(简体) ). 在网络条目上可选(来自微量)_monotonicTime或者说startedDateTime,通过微量转换context-options主播)和控制台条目(来自微量单调)time字段 + 锚 。 追踪缺失时缺席context-options活动。 没有条目offsetMs从时间表中排除tests[].attempts[].network- 一个三级NetworkReport分开 实例 (发生了什么事), 组 (共享形状),以及 机构 (负载). 访问模式 :- 过滤器
network.instances[]以status, (中文(简体) ),redirectFromId, (中文(简体) ),traceId等查找特定事件 - 扫描
network.groups[groupId]用于集合形状信息(resourceType, (中文(简体) ),failureText, (中文(简体) ),wasAborted, (中文(简体) ),occurrenceCount, (中文(简体) ),retainedInstanceCount, (中文(简体) ),suppressedInstanceCount, (中文(简体) ),evictedInstanceCount, (中文(简体) ),firstOffsetMs, (中文(简体) ),lastOffsetMs(中文(简体) ) - 决定
network.bodies[instance.requestBodyRef]编号 :[instance.responseBodyRef]jSON/文本有效载荷(2KB盖有)[truncated]标记,canonicalized: false(v3.0) (中文(简体) ) network.summary提供端到端会计:observedInstances === retainedInstances + instancesDroppedByFilter + instancesDroppedByGroupCap + instancesDroppedByInstanceCap + instancesSuppressedAsDuplicate + instancesEvictedAfterAdmission对于每一个群体,occurrenceCount === retainedInstanceCount + suppressedInstanceCount + evictedInstanceCount
- 过滤器
- 保留政策 - 案件上限为500起,团体上限为200起,尸体上限为100起。 低信号静态资产(script/stylesheet/image/font/media有2xx且无故障)被投放到过滤器. 同一形状的复制被取出样本:前3个总是承认,后1-in-10. 在压力下,驱逐按优先级划分(5xxx > 可操作连接/DNS/TLS故障 > 4xxx > 成功xhr/fatch > 平地中止 > 未知 > 其他已知的 > 静态资产)
<——关系拒绝而不是动摇. tests[].attempts[].network.instances[].{traceId,spanId,requestId,correlationId}- 说吧traceId财务报告和审定财务报表spanId来自响应W3Ctraceparent当可用时。 如果回复无法读取, Datadog 请求标题(x-datadog-trace-id编号 :x-datadog-parent-id优先于相冲突的请求traceparent并实现从无符号小数到零加分小数的正态.requestId财务报告和审定财务报表correlationId来自x-request-id编号 :x-correlation-id。错误的和全零的痕量识别符被拒绝.tests[].attempts[].network.instances[].clientLifecycle- 添加剂chema-v3浏览器生命周期诊断browser-network-lifecycleJSON附着。 记者通过方法,出处,消毒路径模板,以及最近的请求启动时间来加入记录. 生命周期的区别no_response_headers, (中文(简体) ),headers_without_body_completion, (中文(简体) ),network_failure,以及completed,并可以保留提供的请求/答复/补全/失败时间、待时状态、不透明的关联识别符、协议/连接元数据以及编码字节计数.- 重定向 - 说吧
instance.redirectFromId编号 :instance.redirectToId引用兄弟姐妹实例 id; 将图表走入重建链条. tests[].attempts[].failureArtifacts-- 对于失败/出厂/中断的尝试:screenshotBase64(基座64-编码截图,最大为512KB),videoPath(第一个视频附件路径). 无法获取截图或视频时完全关闭tests[].attachments[].path-- 相对于 Playwright 输出 Dirtests[].stdout编号 :tests[].stderr- 上限为4KB[truncated]标记
浏览器生命周期附件合同
生产商应进口 encodeBrowserLifecycleAttachment 而不是定义自己的计划或尺寸限制。 编码器对记录和命令进行消毒 no_response_headers, (中文(简体) ), headers_without_body_completion,以及 network_failure 常规前的记录 completed ,并返回一个保证不超过共享 64 KiB 限制的天体:
import {
encodeBrowserLifecycleAttachment,
type BrowserLifecycleRecord,
} from "@clipboard-health/playwright-reporter-llm";
const records: BrowserLifecycleRecord[] = [];
const encoded = encodeBrowserLifecycleAttachment({ records });
await testInfo.attach("browser-network-lifecycle", {
body: encoded.body,
contentType: "application/json",
});
BROWSER_LIFECYCLE_ATTACHMENT_SCHEMA 曝光版本,最大字节和最大记录. encoded.attachment.truncated 当呼叫者提前报告截取或编码器丢弃无效或超限记录时,即设定;结果也显示、包含和丢弃记录数.
编码器释放出schema 第2版. 每个版本都识别出一个线形; 第2版使用制作方的嵌入式生命周期事件:
{
"schemaVersion": 2,
"truncated": false,
"records": [
{
"method": "GET",
"origin": "https://api.example.com",
"pathTemplate": "/api/v1/:workplaceId/cases",
"playwrightRequestKey": "request-17",
"cdpRequestId": "1234.56",
"loaderId": "loader-1",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"apiGatewayRequestId": "A0XEghTXPHcEScg=",
"requestStarted": {
"cdp": {
"utc": "2026-07-20T18:35:43.100Z",
"monotonicMilliseconds": 12345.1
}
},
"responseReceived": {
"cdp": {
"utc": "2026-07-20T18:35:43.168Z",
"monotonicMilliseconds": 12413.1
}
},
"loadingFinished": {
"cdp": {
"utc": "2026-07-20T18:35:43.170Z",
"monotonicMilliseconds": 12415.1
}
},
"protocol": "h2",
"connection": {
"id": 17,
"reused": true,
"remoteEndpoint": {
"ipAddress": "10.0.0.12",
"port": 443
}
},
"encodedBytes": {
"data": 421,
"responseHeaders": 256,
"total": 677
},
"classification": "completed"
}
]
}
对于失败, loadingFailed 可以提供嵌入式 CDP 或 Playwright 时间戳加 errorText (仅限于铬) net::ERR_* 价值, canceled, (中文(简体) ), blockedReason,以及 corsErrorStatus. 记者喜欢CDP时间戳并倒回Playwright. 未知字段被丢弃 。 原出处减少为方案/主机/端口,查询和从中去除片段文本 pathTemplate,标识符被检查格式/长度,并且只释放了已记录的许可列表.
固定件可以附加编码为 browser-network-lifecycle 或者说 browser-network-lifecycle.json 有内容类型 application/json,直接输入 body 或通过 path。记者独立执行相同的字节和记录限制; clientLifecycle.truncated 标记制片人或记者侧截取。 遗留的平面版本一的读取器仍然用于现有的附件,但新制作者只能通过共享编码器来发出版本二.
为什么Playwright不是内置的JSON记者?
这个图书馆专门供特工使用:
- JSON而不是减分. 其他以LLM为主的记者会发出一个减分摘要,这个摘要容易被读取并难于后处理. 特工们不能廉价地过滤到"只是测试失败"或"只是4xx/5xx请求失败的尝试"而不重新分解出道词. JSON有平地,有记录的图案 让特工
jq或索引到它们需要的字段. - 更好的片状测试诊断. 以统一、时间顺序重试历史
timeline[]步骤、网络和操纵器 失败的尝试与过去的努力之间的分歧通常是诊断(见 flash 测试示例) (中文(简体) ). - 更好的后端追踪关联.
traceId财务报告和审定财务报表spanId从响应W3C解析traceparent头部,带有Datadog request-context为无法读取的回放,因此代理可以从失败测试跳到实际选择的后端上下文. - 更好的信号过滤. 网络优先级保留,控制台过滤,并允许列出标题. 代理在噪音上读取未过滤到的痕量会烧掉活物;这位记者在前面进行过滤.
地方发展指挥部
见 package.json scripts 用于命令列表.