Skip to content

Reporters & output

After every run Blop writes a structured artifact bundle to reportDir (default: .blop/). The bundle is the same regardless of how you trigger the run — local CLI, watch mode, or programmatic runBlopTests.

File Format When
.blop/results.json JSON Every reporter mode
.blop/events.jsonl JSONL Every reporter mode
.blop/report.xml JUnit XML --reporter junit or --reporter all
.blop/screenshots/* PNG When the check captures screenshots or per-action capture is enabled
Terminal window
blop test --reporter all # default; everything
blop test --reporter junit # results.json + events.jsonl + report.xml
blop test --reporter json # results.json + events.jsonl
blop test --reporter basic # results.json + events.jsonl, no JUnit XML
Use case Recommended
Local development all (or omit, it’s the default)
CI with JUnit ingest all or junit
Programmatic consumer reading JSON json
Quick smoke check without JUnit XML basic

Top-level shape (see API reference → Runner for the full type):

{
"runId": "01...",
"status": "passed",
"startedAt": "2026-05-09T12:00:00.000Z",
"finishedAt": "2026-05-09T12:00:42.000Z",
"durationMs": 42_000,
"results": [
{
"id": "01...",
"name": "homepage > loads",
"status": "passed",
"reason": "Homepage rendered with primary CTA visible.",
"startedAt": "...",
"finishedAt": "...",
"durationMs": 12_000,
"attempts": 1,
"baseUrl": "http://localhost:3000",
"provider": "openai",
"model": "gpt-5",
"ci": { "provider": "github-actions", "runId": "...", ... },
"screenshots": [".blop/screenshots/01....png"],
"actions": [ { "name": "browser_goto", "input": {...}, "output": "...", "timestamp": "..." } ],
"events": [ { "event_type": "step_start", ... } ]
}
]
}

The aggregate status rolls up:

  • error if any test errored
  • failed if any test failed (but none errored)
  • passed otherwise

One JSON object per line. Useful for live tailing during a run, post-mortem debugging, or shipping into log aggregators.

{"event_type":"step_start","metadata":{},"timestamp":"..."}
{"event_type":"text_delta","content":"I'll navigate to /","metadata":{},"timestamp":"..."}
{"event_type":"tool_result","content":"navigated","metadata":{"name":"browser_goto"},"timestamp":"..."}
{"event_type":"step_finish","metadata":{},"timestamp":"..."}

The full event schema lives on BlopAgentEvent.

A flat <testsuite> per run, one <testcase> per Blop test. Mapping:

Blop status JUnit element
passed <testcase> (no children)
failed <testcase> with <failure>
error <testcase> with <failure>

Most CI providers (GitHub Actions, GitLab, CircleCI, Buildkite) read this format natively.

Screenshots are recorded when the check calls the screenshot tool or when per-action capture is enabled. File paths are absolute when the runner records them and relative under .blop/screenshots/ when you publish artifacts.

If you want to act on the result instead of reading from disk:

import { runBlopTests } from "@blopai/cli";
const result = await runBlopTests({
specFiles: ["tests/homepage.blop.ts"],
baseUrl: "http://localhost:3000",
reporter: "all",
});
if (result.status !== "passed") {
for (const test of result.results) {
if (test.status !== "passed") console.error(test.name, test.reason);
}
process.exit(1);
}

The runner still writes results.json and events.jsonl to reportDir in every reporter mode.

Blop never removes the report directory between runs. In CI, prefer either:

  • A unique --report-dir per run, e.g. --report-dir .blop/$GITHUB_RUN_ID.
  • An explicit rm -rf .blop step before blop test.

The report directory is regenerated on every run and can grow into the megabytes once screenshots accumulate. Don’t commit it:

# Blop test runs — regenerated on every `blop test`; never commit
.blop/