import { execFile } from "node:child_process"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import type { Exchange, ReportData, StepData } from "./data"; // the render loads web's svelte and tailwind, so it runs in a separate node export const writeHtmlReport = async (out: string, data: ReportData) => { const dir = await mkdtemp(path.join(tmpdir(), "e2e-report-")); const input = path.join(dir, "report.json"); const render = path.join(path.dirname(fileURLToPath(import.meta.url)), "render.ts"); try { await writeFile(input, JSON.stringify(data)); await promisify(execFile)(process.execPath, [render, input, path.resolve(out)], { timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }); console.log(`html report: ${out}`); } catch (error) { const failure = error as { stderr?: string; message: string }; const why = (failure.stderr || failure.message).trim().split("\n").slice(0, 20).join("\n"); console.log(`no html report: ${why}`); } finally { await rm(dir, { recursive: true, force: true }); } }; const pairs = (headers: Record = {}) => Object.entries(headers).map(([name, value]) => ({ name, value })); // only what the steps kept is in the har, for devtools or any har viewer export const writeHar = async (out: string, data: ReportData) => { const exchanges = (steps: StepData[]): Exchange[] => steps.flatMap((step) => [...(step.exchanges ?? []), ...exchanges(step.children)]); const entries = data.specs .flatMap((spec) => spec.flows.flatMap((flow) => exchanges(flow.steps))) .sort((a, b) => (a.at ?? 0) - (b.at ?? 0)) .map((exchange) => { const url = new URL(exchange.url); const type = exchange.response?.headers["content-type"] ?? ""; return { startedDateTime: new Date(exchange.at ?? 0).toISOString(), time: exchange.ms, request: { method: exchange.method, url: exchange.url, httpVersion: "HTTP/1.1", headers: pairs(exchange.request?.headers), queryString: [...url.searchParams].map(([name, value]) => ({ name, value })), cookies: [], headersSize: -1, bodySize: exchange.request?.body?.length ?? 0, ...(exchange.request?.body !== undefined && { postData: { mimeType: exchange.request.headers["content-type"] ?? "", text: exchange.request.body } }) }, response: { status: exchange.status ?? 0, statusText: exchange.error ?? "", httpVersion: "HTTP/1.1", headers: pairs(exchange.response?.headers), cookies: [], content: { size: exchange.response?.body?.length ?? 0, mimeType: type, text: exchange.response?.body ?? "" }, redirectURL: "", headersSize: -1, bodySize: -1 }, cache: {}, timings: { send: 0, wait: exchange.ms, receive: 0 } }; }); try { const log = { version: "1.2", creator: { name: "e2e", version: "1" }, entries }; await writeFile(out, JSON.stringify({ log })); console.log(`har: ${out}`); } catch (error) { console.log(`no har: ${(error as Error).message}`); } };