Reports and artifacts

Per-script and combined Markdown reports pairing each script with its log and images, rendering them to HTML, and redacting the environment first.

A browser test that only reports pass or fail throws away most of what it saw. cdpscripttest can emit a Markdown report pairing each script’s source with its command log and the artifacts it produced — which turns a failing run into something a person, or an agent, can read without rerunning it.

cdpscripttest.GenerateReport(reportPath, name, archive.Comment, log)

The command log is what Engine.Execute writes to its io.Writer, so capture it in a strings.Builder rather than discarding it.

Combined reports

For a suite, one merged report beats a directory of files:

w, err := cdpscripttest.NewCombinedReportWriter(combinedPath, names, sources)
...
w.Update(cdpscripttest.ScriptReport{
	Name:        name,
	Source:      archive.Comment,
	Log:         log,
	ArtifactDir: scriptArtifacts,
	Failed:      failed,
})

Update is called per script as it finishes, so a run that crashes halfway still leaves a readable report covering what completed. GenerateCombinedReport writes them all at once when you already have the full set.

For Go tests, the compatibility flags -emit-cdp-report and -emit-cdp-report-combined write Markdown reports into the normal artifact tree. For a self-contained report tree with native HTML, use:

go test -p 1 -parallel 1 -tags cdp ./cdpscripttest \
  -cdp-report-dir=./report \
  -emit-cdp-report-html \
  -emit-cdp-report-combined

This writes index.md and index.html at ./report, with each fixture’s report.md, report.html, and existing artifacts in ./report/<fixture>/. -cdp-report-dir takes precedence over the usual artifact-root selection. -emit-cdp-report-html writes HTML only when -cdp-report-dir is set.

Rendering to HTML

cdpscripttest/report renders HTML directly with html/template. HTML pages use relative links to the existing artifact tree, so no converter or asset-copy step is required.

What ends up in a report, and what to redact

GenerateReport takes the script source and the execution log. It does not serialize the environment, so a credential in os.Environ() does not land in a report merely by being there.

The leak path is the log. Anything a script prints is captured, so a fixture that runs env, echoes a variable, or evaluates JavaScript that returns a token puts that value into the report — which then gets committed, rendered to HTML, and attached to pull requests.

Two defenses, and you want both. Do not print secrets from a fixture. And when a run’s report will be shared, filter the environment you hand the runner so a careless env cannot expose anything:

func sanitize(env []string) []string {
	out := make([]string, 0, len(env))
	for _, kv := range env {
		k, v, ok := strings.Cut(kv, "=")
		if !ok {
			continue
		}
		if sensitive(k) {
			out = append(out, k+"=[redacted]")
			continue
		}
		out = append(out, k+"="+v)
	}
	return out
}

Treat a key as sensitive when it contains TOKEN, SECRET, PASSWORD, COOKIE, or PRIVATE_KEY, or ends in _KEY or _API. That list is a floor, not a guarantee — a key named github_pat passes all of those checks. Prefer an allowlist when the report is published.

The same caution applies to captures: a HAR of an authenticated session contains the headers that made it authenticated. See Capture network traffic.

Coverage

Native fixture execution starts CDP coverage collection by default and writes coverage.json into each script’s artifact directory. That is why the file appears next to your screenshots without anyone asking for it.

CDPSCRIPTTEST_COVERAGE=0 go test -tags cdp ./...   # also 'false', 'off'

Coverage only records JavaScript that executed, so a server-rendered page with little script produces a near-empty file — expected, not a failure.

Artifact layout

Screenshots, baselines, GIFs, and reports share one artifact directory per script. Resolution order and the flat CDPSCRIPTTEST_ARTIFACTS layout are covered in Visual testing.

Keep the artifact root out of the committed tree unless it holds baselines you mean to keep. A run that clears its artifact root each time cannot accumulate baselines — every comparison becomes a first run, and first runs always pass.

Next steps

Last updated: 2026-09-23