Playwright 报告器配置:内置几种怎么选、怎么同时输出多份
跑测试这件事,本地和 CI 想看的东西根本不是一回事。本地你希望一条一条地看着用例过去,哪个卡住了一眼能认出来;CI 上你不想要几百行滚屏,你要的是一个能上传成 artifact 的文件,最好还能被别的系统解析。很多人的做法是在两个地方各写一套脚本,或者干脆在 CI 里把输出重定向到文件再自己解析——这两件事 Playwright Test 的 reporter 机制本来就管,配置一行就能分开。
这篇把仓库里 docs/src/test-reporters-js.md 和 docs/src/test-reporter-api/class-reporter.md 两份文档串起来讲:内置的几种各自吐出什么、怎么同时挂多个、以及自己写一个 reporter 时要实现哪些方法。该项目持续更新,下面涉及的配置项名、默认值与 API 签名随版本变动,请以仓库最新内容为准。
前置条件:这套东西只在 JS/TS 绑定里有
这一段容易被跳过,但它决定了你后面看到的写法能不能用。
docs/src 目录下报告器的指南文件只有一个 test-reporters-js.md,没有 -python、-java、-csharp 的同名文件;docs/src/test-reporter-api/class-reporter.md 的头部标注是 langs: js。也就是说,reporter 这个配置项、以及自定义 reporter 的那套钩子接口,是 Playwright Test(JS/TS 的测试运行器)这一侧的东西。你在 Python 或 .NET 绑定里找不到对应的配置,别照着抄。
其次,自定义 reporter 的类型不是从 @playwright/test 主入口导入的,而是从子路径导入。仓库示例里写的是:
import type {
Reporter, FullConfig, Suite, TestCase, TestResult, FullResult
} from '@playwright/test/reporter';
这个 /reporter 子路径经常被写漏,写成主入口会直接找不到类型。
内置报告器:先看它们各自产出什么
docs/src/test-api/class-testconfig.md 里 TestConfig.reporter 的类型标注给出了 BuiltInReporter 的取值,一共八个:list、dot、line、github、json、junit、null、html。指南文档 test-reporters-js.md 里另外还单列了一节 Blob reporter,并给出了 reporter: 'blob' 的配置示例;merge-reports 命令的 --reporter 选项说明里也把 blob 列在可选值中。再往源码里翻一层,packages/playwright/src/common/config.ts 导出的 builtInReporters 常量里是带 blob 的,packages/playwright/src/runner/reporters.ts 的 defaultReporters 映射也把 blob 映到了 BlobReporter。也就是说 blob 在指南、CLI 与源码三处都在,只是没有出现在 TestConfig.reporter 那行类型标注的枚举里——这个差异说到这里为止,仓库里没写为什么,我们不替它解释。
按产出形态,它们大致分三类。
打到终端上的:list 给每个正在跑的用例打一行,是本地默认;line 用固定的一行滚动显示最后完成的用例,失败时就地把错误打出来,文档里说它适合用例特别多、不想被刷屏的场景;dot 每个跑完的用例只输出一个字符,是 CI 上的默认。dot 的那个字符表值得记一下,因为不看表根本猜不出来:
| 字符 | 含义 |
|---|---|
· | Passed |
F | Failed |
× | Failed or timed out - and will be retried |
± | Passed on retry (flaky) |
T | Timed out |
° | Skipped |
× 和 F 的区别是「会不会再重试」,很多人第一次看到一串点里夹着 × 会以为是编码问题。
落成文件给机器读的:json 产出一个包含整次运行信息的对象;junit 产出 JUnit 风格的 xml;html 产出一个自包含的目录,可以当网页托管;blob 产出 zip,文档里写明它的主要用途是把分片跑出来的多份报告合并成别的报告。
给平台看的:github 是在 GitHub Actions 里产生失败注解用的。文档里同时提醒了一句:其它 reporter 在 GitHub Actions 上也都能用,只是没有注解;并且不建议在 matrix 策略下用这种注解类型,因为堆栈失败会成倍出现、把文件视图淹掉。
还有一个 null。reporters.ts 里它映到的类来自 packages/playwright/src/reporters/empty.ts,那个 EmptyReporter 类通篇只实现了 version() 与 printsToStdio()(返回 false),别的钩子一个都没有。
同时输出多份:数组式配置
命令行上试最快,--reporter 是逗号分隔的:
npx playwright test --reporter=line
但要给 reporter 传选项就得写到配置文件里。单个 reporter 直接写字符串:
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: 'line',
});
要同时挂多个,就把 reporter 写成数组,每一项是一个 [名字] 或 [名字, 选项对象] 的元组。仓库里给的例子正是「终端好看的 + 机器能读的」这个组合:
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['list'],
['json', { outputFile: 'test-results.json' }]
],
});
这里的 test-results.json 只是仓库文档里的示例值,不是什么约定俗成的名字,换成你自己的路径即可。
本地和 CI 用不同的 reporter,文档给的写法是直接读环境变量分支:
export default defineConfig({
// Concise 'dot' for CI, default 'list' when running locally
reporter: process.env.CI ? 'dot' : 'list',
});
顺带说清默认值:TestConfig.reporter 的说明写明,不指定这个属性时,设置了 CI 环境变量就用 dot,否则用 list。这是仓库当前文档里的默认值,随版本可能变动。另外 docs/src/test-cli-js.md 里 playwright test 的 --reporter 选项那一行写的是 (default: "list"),和配置文档那句「CI 下是 dot」的措辞不完全一致,两处并列在这里,我们不再往下推断。
还有一个容易被忽略的命令行选项 --add-reporter:文档写明它是在配置文件里已经配好的 reporter 之上再加一个,逗号分隔,可以是内置名也可以是自定义 reporter 文件路径;和 --reporter 的区别就是它保留已配置的那些而不是替换掉。临时想在某次 CI 跑里额外多要一份产物,用它比改配置干净:
npx playwright test --add-reporter=json
以上为按仓库文档中的参数语义组合的示例,未经实测,以仓库最新内容与 --help 的实际输出为准。
输出位置:Windows 上别照抄 Linux 那行
绝大部分内置 reporter 既能用配置项也能用环境变量指定输出,两条路都在文档表格里。以 json 为例,配置里是 outputFile,环境变量是 PLAYWRIGHT_JSON_OUTPUT_NAME。仓库文档为环境变量这条路分别给了三种 shell 的写法,原文如下——
Linux / macOS 的 bash:
PLAYWRIGHT_JSON_OUTPUT_NAME=results.json npx playwright test --reporter=json
Windows 的 cmd:
set PLAYWRIGHT_JSON_OUTPUT_NAME=results.json
npx playwright test --reporter=json
Windows 的 PowerShell:
$env:PLAYWRIGHT_JSON_OUTPUT_NAME="results.json"
npx playwright test --reporter=json
bash 那种「变量写在命令前面」的前缀写法在 cmd 和 PowerShell 里是不成立的,必须先单独设置再执行。这类问题在本地能跑、一进 Windows 的 CI runner 就哑火,排查起来很费时间。
几个默认输出位置也记一下:html 默认写到当前工作目录下的 playwright-report 文件夹,可以用 outputFolder 选项或 PLAYWRIGHT_HTML_OUTPUT_DIR 覆盖;blob 默认写到 blob-report 目录(文档写的是 package.json 所在目录,找不到 package.json 时用当前工作目录)。这些是仓库当前文档里的默认值,随版本可能变动。
自定义 reporter:实现哪些钩子
自定义 reporter 就是一个类,把它作为默认导出。class-reporter.md 里明确写着「所有方法都是可选的」,你只实现关心的那几个即可。仓库给的最小例子:
class MyReporter implements Reporter {
onBegin(config: FullConfig, suite: Suite) {
console.log(`Starting the run with ${suite.allTests().length} tests`);
}
onTestBegin(test: TestCase) {
console.log(`Starting test ${test.title}`);
}
onTestEnd(test: TestCase, result: TestResult) {
console.log(`Finished test ${test.title}: ${result.status}`);
}
onEnd(result: FullResult) {
console.log(`Finished the run: ${result.status}`);
}
}
export default MyReporter;
挂上去的方式和内置的一样,写文件路径;要传选项就用元组形式,选项会进构造函数:
export default defineConfig({
reporter: [['./my-awesome-reporter.ts', { customOption: 'some value' }]],
});
文档给出的典型调用顺序是:onBegin 只调一次,拿到根 Suite;每个用例开始时 onTestBegin,此时给你的 TestResult 几乎是空的,会随着用例执行被逐步填充;用例内部每个 step 触发 onStepBegin / onStepEnd,这时候用例还没结束;用例跑完调 onTestEnd,这时 TestResult 才完整,status、error 这些才可用;全部跑完调一次 onEnd;最后在测试运行器退出前调 onExit。
另外三个是旁路的:worker 进程写标准输出和标准错误时分别触发 onStdOut / onStdErr,它们的 test 和 result 参数类型都是 void | TestCase、void | TestResult——文档解释说输出可能发生在没有用例在跑的时候;用例执行之外出了问题则触发 onError。
几个值得单独拎出来的点:
onEnd是 async 的,可以返回 Promise,Playwright 会 await;文档还写明 reporter 被允许覆写status,从而影响测试运行器的退出码。这是个挺重的权限,写的时候留神。onExit自 v1.33 起可用。文档写明调到它的时候所有 reporter 都已经收到过onEnd、报告都该生成完了,所以上传报告的代码适合放这里。- 如果你的 reporter 压根不往终端打字,实现
printsToStdio并返回false。文档说这样 Playwright 会额外挂一个标准终端 reporter,免得你面对一片空白。 onError的workerInfo参数自 v1.60 起可用,是可选参数,与具体 worker 无关的错误上它是undefined。preprocess自 v1.62 起可用,在配置解析完、onBegin之前调用,参数里带一个TestRun,可以把用例标成 skip、exclude、fail、fixme。这已经不只是「报告」了,是能改运行内容的。
以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。
边界:文档明说的几件事
reporter 里抛错会被吞掉。 class-reporter.md 有一节标题就是 Reporter errors,写明 Playwright 会吞掉自定义 reporter 方法里抛出的任何异常;你要想发现或者据此让运行失败,得自己包一层处理。这条不写清楚,调试自定义 reporter 时会一直以为是钩子没被调用。
合并报告时同名 project 会有多份。 用 merge-reports 合并多个 blob 报告时走的是同一套 Reporter API,文档说现有 reporter 基本不用改,但有个差别:不同分片的 project 始终是各自独立的 TestProject 对象,同一个 project 名字被分到几台机器上跑,传给 onBegin 的配置里就会有几个同名实例。自定义 reporter 里如果按 project 名做过去重或聚合,这里会出问题。
preprocess 拿到的 suite 忽略分片。 文档写明它反映了 --project、--grep / --grep-invert 和 .only 的过滤,但会忽略 --shard:它拿到的始终是完整的、未分片的全集,Playwright 在 preprocess 返回之后才应用内置分片,除非 reporter 调用了 TestRun.skipSharding。另外 setup 与依赖 project 是只读的,不能通过 TestRun 改。
多个 reporter 之间按数组顺序被逐个调用。 这一点指南文档没有明写,但源码里看得到:packages/playwright/src/reporters/multiplexer.ts 的 Multiplexer 类持有一组 reporter,每个钩子方法里都是 for (const reporter of this._reporters) 挨个转发;而这组 reporter 是 reporters.ts 的 createReporters 按配置里 descriptions 数组的顺序依次构造出来的。上面说的「异常被吞掉」在这里也能对上——Multiplexer 的 _wrap / _wrapAsync 用 try/catch 包住每次转发,捕获到就置 _hasReporterErrors 并转成 onError,不往外抛。
怎么验证配对了
最直接的是 html 那条路。跑完之后用仓库文档给的命令打开上次的报告:
npx playwright show-report
如果你改过输出目录,把目录名跟在后面。文档还写明它接受 .zip 归档——比如从 CI artifact 下载下来的那个,前提是压缩包顶层要有 index.html,Playwright 会解压到临时目录再托管:
npx playwright show-report playwright-report.zip
数组式多 reporter 配对没配对,看文件系统就知道:你在 outputFile 里写的那个路径有没有生成,终端上那份该有的输出还在不在。两边都在,说明数组里的每一项都被挂上了;只剩终端输出,通常是你把数组写成了单个字符串,后面那项根本没进配置。
自定义 reporter 那侧,先在 onBegin 里打一行,确认它被加载了;确认不了就先怀疑路径,再想别的——毕竟异常是被吞掉的,看不到报错并不代表没报错。
本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测,
因此不涉及运行速度、稳定性与实际表现的任何描述。
该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。
本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。