donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
124 lines • 5.91 kB
JavaScript
;
/**
* @fileoverview Reconstructs a `DonobuReport` from live Playwright `Reporter`
* events. Shared across all Donobu reporter classes (HTML, Markdown, Slack)
* so they all agree on the canonical shape sent downstream to the renderers
* and the auto-heal merge step.
*
* The output structure mirrors Playwright's native JSON reporter:
* suites (one per file) → specs (one per test title) → tests (one per project)
* plus the Donobu-specific `metadata` fields the merge + render steps rely on.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildDonobuReport = buildDonobuReport;
const path_1 = require("path");
/**
* Build a canonical `DonobuReport` from the per-test result accumulators that
* each Donobu reporter maintains during a run.
*
* Callers are expected to fill in `metadata` (specifically the `donobuOutputs`
* entry for their format) before persisting the result — this helper owns the
* structure walk, not the output-path accounting.
*/
function buildDonobuReport(resultsByTest, rootDir) {
// Group tests by file path, then by test title.
// Multiple TestCase objects with the same (file, title) represent the same
// spec running under different Playwright projects.
// Paths are normalized to be relative to the Playwright rootDir (mirroring
// the native JSON reporter) so GitHub Actions summaries and other consumers
// don't show absolute CI runner paths.
const byFile = new Map();
for (const test of resultsByTest.keys()) {
const file = rootDir
? (0, path_1.relative)(rootDir, test.location.file)
: test.location.file;
if (!byFile.has(file)) {
byFile.set(file, new Map());
}
const byTitle = byFile.get(file);
if (!byTitle.has(test.title)) {
byTitle.set(test.title, []);
}
byTitle.get(test.title).push(test);
}
const suites = [];
for (const [file, titleMap] of byFile) {
const specs = [];
for (const [title, tests] of titleMap) {
const testEntries = tests.map((test) => {
const results = resultsByTest.get(test) ?? [];
return {
// Playwright's stable per-(file, project, title) ID — used by the
// merge step's `byId` index and by the HTML renderer for permalink
// anchors (`index.html#?testId=<id>`) and the per-test redirect stubs.
testId: test.id,
annotations: test.annotations,
tags: test.tags,
projectName: getProjectName(test),
// Signal "skipped" tests to the renderers the same way the JSON
// reporter does.
status: test.expectedStatus === 'skipped' ? 'skipped' : undefined,
results: results.map((r) => ({
status: r.status,
duration: r.duration,
retry: r.retry,
startTime: r.startTime?.toISOString() ?? null,
// Pass errors through; cast to any to preserve runtime-only
// fields (snippet, actual, expected) that Playwright adds to
// assertion errors.
errors: r.errors.map((e) => ({
message: e.message,
stack: e.stack,
snippet: e.snippet,
actual: e.actual,
expected: e.expected,
location: e.location,
})),
// The markdown renderer reads `result.error` (singular) in addition
// to `errors[]`; surface the first error there for back-compat.
error: r.errors[0]
? {
message: r.errors[0].message,
stack: r.errors[0].stack,
snippet: r.errors[0].snippet,
}
: undefined,
attachments: r.attachments.map((a) => ({
name: a.name,
contentType: a.contentType,
path: a.path ?? null,
// Playwright Reporter API provides body as a Buffer; the HTML
// generator expects a base64-encoded string (matching JSON
// format). When `path` is set the renderer prefers it, so skip
// the base64 inflation — large image attachments end up as
// sidecar files on disk rather than embedded in the report.
body: a.path
? undefined
: a.body
? a.body.toString('base64')
: undefined,
})),
// TestResult.stderr is already Array<{text?: string; buffer?: string}>,
// which is the same shape parseStderrSteps expects.
stderr: r.stderr,
})),
};
});
specs.push({ title, tests: testEntries });
}
suites.push({ file, specs });
}
return { suites, metadata: {} };
}
/** Walk up the suite chain to find the enclosing project suite's title. */
function getProjectName(test) {
let suite = test.parent;
while (suite) {
if (suite.type === 'project') {
return suite.title;
}
suite = suite.parent;
}
return '';
}
//# sourceMappingURL=buildReport.js.map