Reporters
A run publishes events to every configured reporter at once. Pick them
on the command line or in the config file; they compose freely, so a CI
job can print a live line view, annotate the PR, and write JUnit XML
from one run.
Options may sit beside name, as above, or in an explicit table — both
are read, and the table wins on a collision:
A reporter with no options can be named on its own:
Built-in reporters
Every terminal reporter ends a run the same way: the numbered failure
bodies, the slow-file warning, then the counts. A dot run that fails
prints the same diagnostics a list run does.
Where files go
A file reporter resolves its path in this order, first match wins:
FERRIDRIVER_<REPORTER>_OUTPUT_FILE— e.g.FERRIDRIVER_JUNIT_OUTPUT_FILE=/tmp/j.xml- the reporter's
outputFileoption FERRIDRIVER_<REPORTER>_OUTPUT_DIRor theoutputDiroption, joined withFERRIDRIVER_<REPORTER>_OUTPUT_NAMEor thefileNameoption- the run's
outputDirplus the reporter's default name
The environment variables are what a CI matrix uses to give each shard its own file without editing the config.
Reporter options
Options go beside the reporter's name (or in its options table), and
boolean ones can also be flipped from the environment as
FERRIDRIVER_<REPORTER>_<OPTION> (e.g.
FERRIDRIVER_JUNIT_INCLUDE_RETRIES=1).
junit
html
blob
allure
Sharded runs and merge-reports
Each shard writes a blob; one merge step turns them into the report the unsharded run would have produced.
The blob is lossless: steps, attachments, annotations, stacks, worker indexes and start times all round-trip, so a merged HTML or JUnit report carries everything a direct run's would. Inline artifacts (a failure screenshot) are stored as entries inside the zip rather than inlined into the event stream.
merge-reports exits non-zero when any test in the merged run failed,
so it gates a pipeline the same way the shards do.
Writing to CI
github emits an annotation per failure pointing at the line the error
actually came from — parsed out of the stack, not the test's declaration
— plus a run-summary notice and a warning per slow file. It wraps the
list reporter, so the job log stays readable:
markdown writes report.md and, when GITHUB_STEP_SUMMARY is set,
appends the same text there — the summary renders on the job page with
no extra workflow step.
Custom reporters
A reporter name that is not one of the built-ins is a path to a module,
resolved against the working directory and then against testDir. The
module's default export is the reporter class; it is bundled and
compiled through the same TypeScript pipeline the specs take, and
constructed once per run with the entry's own options plus configDir
and outputDir:
A reporter runs in the same sandbox every other script does: fs is
rooted at the project directory and fetch obeys the [scripting]
network policy, so a reporter that posts to a webhook needs that host
allowed like any other outbound call.
The module is compiled before the first test runs, so a reporter that does not resolve, does not parse, or throws while constructing fails the command rather than the run. Once the run has started, a reporter is isolated: a hook that throws is reported and the run carries on, exactly as it does upstream.
The two interfaces
There are two, and which one a class implements is decided by
version():
Everything else is the same. Reporter is the interface third-party
Playwright reporters implement, so an existing one works unchanged.
What a hook is handed
The objects are Playwright's: a Suite tree of root → project → file →
describe with titlePath(), entries(), allTests() and project();
a TestCase with outcome() and ok() computed from the results it
has so far; a TestResult whose status is undefined until the
attempt ends, and whose steps fill in as onStepBegin fires; and
attachments whose body is a Buffer.
Deciding whether the run printed anything
printsToStdio() says whether the reporter writes to the terminal. If
nothing in the whole set does — a run configured with only json and a
custom file reporter, say — a line reporter (or dot under CI) is put
in FRONT of the set, so the run is never silent. A reporter that does
not declare printsToStdio counts as one that prints.
Editing the run, and deciding how it ended
preprocess({ config, suite, testRun }) runs once before the first
test with the whole corpus in hand. testRun.exclude(target) drops a
case or a whole suite, testRun.skip / fixme / fail annotate one,
and testRun.skipSharding() says the reporter has taken sharding over.
Unlike every other hook, an error here is not swallowed — it aborts the
run rather than letting a half-applied edit reach the workers. The
handle is dead the moment preprocess returns.
onEnd(result) may return { status } to change how the run is
reported to have ended, and the process exit code follows it. The last
reporter to answer wins.