(() => { "use strict"; const MAX_RENDERED_RESULTS = 2_000; const MAX_DETAIL_CHARS = 24_000; const MAX_HIGHLIGHTED_RAW_CHARS = 4 * 1024 * 1024; const rawSources = new WeakMap(); function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function icon(name) { const safeName = /^[a-z-]+$/.test(name) ? name : "file"; return ``; } function formatDate(value) { const date = new Date(value); return Number.isNaN(date.getTime()) ? "" : new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short", }).format(date); } function elements(parent, localName) { if (!parent?.getElementsByTagName) return []; return [...parent.getElementsByTagName("*")].filter( (element) => element.localName === localName, ); } function firstElement(parent, localName) { return elements(parent, localName)[0] ?? null; } function directChild(parent, localName) { return ( [...(parent?.children ?? [])].find( (element) => element.localName === localName, ) ?? null ); } function elementText(element) { return String(element?.textContent ?? "").trim(); } function attribute(element, name) { return String(element?.getAttribute(name) ?? "").trim(); } function outcomeCategory(outcome) { const normalized = String(outcome ?? "") .toLocaleLowerCase() .replace(/[^a-z]/g, ""); if (normalized === "passed") return "passed"; if ( [ "failed", "error", "timeout", "aborted", "passedbutrunaborted", "disconnected", ].includes(normalized) ) { return "failed"; } if ( ["notexecuted", "notrunnable", "inconclusive", "pending"].includes( normalized, ) ) { return "skipped"; } return "other"; } function outcomeIcon(category) { if (category === "passed") return "check-circle"; if (category === "failed") return "x-circle"; if (category === "skipped") return "clock"; return "workflow"; } const KNOWN_TRX_OUTCOMES = new Set([ "passed", "failed", "completed", "error", "warning", "inconclusive", "aborted", "timeout", "inprogress", "notexecuted", ]); function summarizedRunOutcome(report) { const normalized = String(report.outcome ?? "").toLowerCase(); if (!KNOWN_TRX_OUTCOMES.has(normalized) || normalized === "completed") { if (report.summaryCounts.failed > 0) return "Failed"; if (report.summaryCounts.passed > 0) return "Passed"; if (report.summaryCounts.skipped > 0) return "Skipped"; } return report.outcome; } function parseDuration(value) { const duration = String(value ?? "").trim(); if (!duration) return null; const clock = duration.match( /^(?:(\d+)\.)?(\d{1,2}):(\d{2}):(\d{2}(?:\.\d+)?)$/, ); if (clock) { const [, days = "0", hours, minutes, seconds] = clock; return ( Number(days) * 86_400_000 + Number(hours) * 3_600_000 + Number(minutes) * 60_000 + Number(seconds) * 1_000 ); } const iso = duration.match( /^P(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/i, ); if (!iso) return null; const [, days = "0", hours = "0", minutes = "0", seconds = "0"] = iso; return ( Number(days) * 86_400_000 + Number(hours) * 3_600_000 + Number(minutes) * 60_000 + Number(seconds) * 1_000 ); } function formatDuration(durationMs) { if (!Number.isFinite(durationMs)) return ""; if (durationMs < 1_000) { return `${Math.max(0, Math.round(durationMs))} ms`; } if (durationMs < 60_000) { const seconds = durationMs / 1_000; return `${seconds .toFixed(seconds < 10 ? 2 : 1) .replace(/\.0+$|(\.\d*[1-9])0+$/, "$1")} s`; } if (durationMs < 3_600_000) { const minutes = Math.floor(durationMs / 60_000); const seconds = Math.floor((durationMs % 60_000) / 1_000); return `${minutes}m ${seconds}s`; } const hours = Math.floor(durationMs / 3_600_000); const minutes = Math.floor((durationMs % 3_600_000) / 60_000); return `${hours}h ${minutes}m`; } function parseReport(value) { const source = String(value ?? ""); if (/ element.localName?.toLocaleLowerCase() === "parsererror", ); if (parserError) { const detail = elementText(parserError).replace(/\s+/g, " ").slice(0, 240); throw new Error(detail || "The XML document is malformed."); } const root = documentNode.documentElement; if (root?.localName !== "TestRun") { throw new Error( "The XML document does not contain a TRX TestRun root element.", ); } const definitions = new Map(); for (const unitTest of elements(root, "UnitTest")) { const testId = attribute(unitTest, "id").toLocaleLowerCase(); if (!testId) continue; const method = firstElement(unitTest, "TestMethod"); definitions.set(testId, { name: attribute(unitTest, "name"), className: attribute(method, "className"), methodName: attribute(method, "name"), codeBase: attribute(method, "codeBase"), adapterTypeName: attribute(method, "adapterTypeName"), }); } const testLists = new Map(); for (const testList of elements(root, "TestList")) { const id = attribute(testList, "id").toLocaleLowerCase(); if (id) testLists.set(id, attribute(testList, "name")); } const results = []; const resultsElement = directChild(root, "Results"); const isResultElement = (element) => element.localName?.endsWith("TestResult") || element.localName === "TestResultAggregation"; const readResult = (element, depth) => { const testId = attribute(element, "testId").toLocaleLowerCase(); const definition = definitions.get(testId) ?? {}; const output = directChild(element, "Output"); const errorInfo = firstElement(output, "ErrorInfo"); const outcome = attribute(element, "outcome") || "Unknown"; const testListId = attribute(element, "testListId").toLocaleLowerCase(); const durationValue = attribute(element, "duration"); const resultFiles = elements(output, "ResultFile") .map((file) => attribute(file, "path")) .filter(Boolean); const properties = elements(output, "Property") .map((property) => { const key = elementText(directChild(property, "Key")) || attribute(property, "name"); const propertyValue = elementText(directChild(property, "Value")) || attribute(property, "value"); return key ? { key, value: propertyValue } : null; }) .filter(Boolean); return { depth, outcome, category: outcomeCategory(outcome), testName: attribute(element, "testName") || definition.name || definition.methodName || "Unnamed test", className: definition.className || attribute(element, "className"), methodName: definition.methodName, codeBase: definition.codeBase, adapterTypeName: definition.adapterTypeName, computerName: attribute(element, "computerName"), startTime: attribute(element, "startTime"), endTime: attribute(element, "endTime"), durationMs: parseDuration(durationValue), executionId: attribute(element, "executionId"), testId: attribute(element, "testId"), testListName: testLists.get(testListId) ?? "", dataRowInfo: attribute(element, "dataRowInfo"), message: elementText(firstElement(errorInfo, "Message")), stackTrace: elementText(firstElement(errorInfo, "StackTrace")), stdout: elementText(firstElement(output, "StdOut")), stderr: elementText(firstElement(output, "StdErr")), debugTrace: elementText(firstElement(output, "DebugTrace")), resultFiles, properties, }; }; const visitResults = (parent, depth = 0) => { for (const element of parent?.children ?? []) { if (!isResultElement(element)) continue; results.push(readResult(element, depth)); const innerResults = directChild(element, "InnerResults"); if (innerResults) visitResults(innerResults, depth + 1); } }; visitResults(resultsElement); const countersElement = firstElement(root, "Counters"); const counters = {}; for (const counterAttribute of countersElement?.attributes ?? []) { const number = Number(counterAttribute.value); if (Number.isFinite(number) && number >= 0) { counters[counterAttribute.name] = number; } } const resultCounts = results.reduce( (counts, result) => { counts[result.category] += 1; return counts; }, { passed: 0, failed: 0, skipped: 0, other: 0 }, ); const sumCounters = (names, fallback) => { const values = names .filter((name) => Number.isFinite(counters[name])) .map((name) => counters[name]); return values.length > 0 ? values.reduce((total, number) => total + number, 0) : fallback; }; const times = firstElement(root, "Times"); const startTime = attribute(times, "start"); const finishTime = attribute(times, "finish"); const startTimestamp = new Date(startTime).getTime(); const finishTimestamp = new Date(finishTime).getTime(); const durationMs = Number.isFinite(startTimestamp) && Number.isFinite(finishTimestamp) && finishTimestamp >= startTimestamp ? finishTimestamp - startTimestamp : null; const summaryElement = firstElement(root, "ResultSummary"); return { name: attribute(root, "name") || "Test run", runId: attribute(root, "id"), user: attribute(root, "runUser"), computerName: attribute(root, "computerName"), outcome: attribute(summaryElement, "outcome") || "Unknown", startTime, finishTime, durationMs, resultCounts, summaryCounts: { total: Number.isFinite(counters.total) ? counters.total : results.length, passed: sumCounters(["passed"], resultCounts.passed), failed: sumCounters( [ "failed", "error", "timeout", "aborted", "passedButRunAborted", "disconnected", ], resultCounts.failed, ), skipped: sumCounters( ["notExecuted", "notRunnable", "inconclusive", "pending"], resultCounts.skipped, ), }, results, }; } function renderMetadataItem(label, value, { mono = false } = {}) { if (!value) return ""; return `
${escapeHtml(label)}
${escapeHtml(value)}
`; } function truncateDetail(value) { const text = String(value ?? ""); if (text.length <= MAX_DETAIL_CHARS) { return { text, truncated: false }; } return { text: text.slice(0, MAX_DETAIL_CHARS), truncated: true, }; } function renderDetailBlock(label, value) { if (!value) return ""; const detail = truncateDetail(value); return `

${escapeHtml(label)}

${escapeHtml(detail.text)}
${ detail.truncated ? `

Output truncated after ${MAX_DETAIL_CHARS.toLocaleString()} characters. View the raw XML for the full value.

` : "" }
`; } function renderResult(result, open = false) { const secondaryParts = []; if (result.className) secondaryParts.push(result.className); if (result.dataRowInfo) secondaryParts.push(`Data row ${result.dataRowInfo}`); if (result.category === "failed" && result.message) { secondaryParts.push( result.message.length > 180 ? `${result.message.slice(0, 179)}...` : result.message, ); } const searchValue = [ result.testName, result.className, result.methodName, result.outcome, result.message, ] .filter(Boolean) .join(" ") .toLocaleLowerCase() .slice(0, 2_000); const resultDuration = formatDuration(result.durationMs); const metadata = [ renderMetadataItem("Class", result.className, { mono: true }), renderMetadataItem("Method", result.methodName, { mono: true }), renderMetadataItem("Duration", resultDuration), renderMetadataItem("Computer", result.computerName), renderMetadataItem( "Started", result.startTime ? formatDate(result.startTime) : "", ), renderMetadataItem( "Finished", result.endTime ? formatDate(result.endTime) : "", ), renderMetadataItem("Test list", result.testListName), renderMetadataItem("Adapter", result.adapterTypeName, { mono: true }), renderMetadataItem("Code base", result.codeBase, { mono: true }), renderMetadataItem("Test ID", result.testId, { mono: true }), renderMetadataItem("Execution ID", result.executionId, { mono: true }), ].join(""); const resultFiles = result.resultFiles.length > 0 ? `

Result files

` : ""; const properties = result.properties.length > 0 ? `

Properties

${result.properties .map( ({ key, value }) => `
${escapeHtml(key)}
${escapeHtml(value)}
`, ) .join("")}
` : ""; const details = [ metadata ? `
${metadata}
` : "", renderDetailBlock("Error message", result.message), renderDetailBlock("Stack trace", result.stackTrace), renderDetailBlock("Standard output", result.stdout), renderDetailBlock("Standard error", result.stderr), renderDetailBlock("Debug trace", result.debugTrace), resultFiles, properties, ].join(""); return `
${icon("chevron-right")} ${icon(outcomeIcon(result.category))} ${escapeHtml(result.testName)} ${ secondaryParts.length > 0 ? `${escapeHtml(secondaryParts.join(" | "))}` : "" } ${escapeHtml(resultDuration)} ${escapeHtml(result.outcome)}
${details || '

No additional details were recorded for this result.

'}
`; } function renderMetric(label, value, category, iconName) { return `
${icon(iconName)} ${Number(value).toLocaleString()} ${escapeHtml(label)}
`; } function renderFilter(label, category, count, pressed = false) { return ` `; } function renderParseError(source, message, highlightXml) { const highlighted = source.length <= MAX_HIGHLIGHTED_RAW_CHARS ? highlightXml(source) : escapeHtml(source); return `
Fallback

Raw XML

${highlighted}
`; } function render(value, preview, helpers = {}) { const source = String(value ?? ""); const highlightXml = typeof helpers.highlightXml === "function" ? helpers.highlightXml : escapeHtml; let report; try { report = parseReport(source); } catch (error) { preview.innerHTML = renderParseError( source, error instanceof Error ? error.message : "The TRX document is invalid.", highlightXml, ); return; } const categoryOrder = { failed: 0, other: 1, skipped: 2, passed: 3 }; const renderedResults = [...report.results] .map((result, index) => ({ result, index })) .sort( (left, right) => categoryOrder[left.result.category] - categoryOrder[right.result.category] || left.index - right.index, ) .slice(0, MAX_RENDERED_RESULTS) .map(({ result }) => result); const runOutcome = summarizedRunOutcome(report); const outcome = outcomeCategory(runOutcome); const limited = renderedResults.length < report.results.length; const runDuration = formatDuration(report.durationMs); const runMetadata = [ renderMetadataItem( "Started", report.startTime ? formatDate(report.startTime) : "", ), renderMetadataItem( "Finished", report.finishTime ? formatDate(report.finishTime) : "", ), renderMetadataItem("Duration", runDuration), renderMetadataItem("Run by", report.user), renderMetadataItem("Computer", report.computerName), renderMetadataItem("Run ID", report.runId, { mono: true }), ].join(""); preview.innerHTML = `
Test run

${escapeHtml(report.name)}

${icon(outcomeIcon(outcome))} ${escapeHtml(runOutcome)}
${renderMetric("Total", report.summaryCounts.total, "all", "workflow")} ${renderMetric("Passed", report.summaryCounts.passed, "passed", "check-circle")} ${renderMetric("Failed", report.summaryCounts.failed, "failed", "x-circle")} ${renderMetric("Skipped", report.summaryCounts.skipped, "skipped", "clock")}
${runMetadata ? `` : ""}
Details

Test results

${renderFilter("All", "all", report.results.length, true)} ${renderFilter("Passed", "passed", report.resultCounts.passed)} ${renderFilter("Failed", "failed", report.resultCounts.failed)} ${renderFilter("Skipped", "skipped", report.resultCounts.skipped)} ${renderFilter("Other", "other", report.resultCounts.other)}
${ limited ? `
${icon("eye")} Showing the first ${renderedResults.length.toLocaleString()} of ${report.results.length.toLocaleString()} result records to keep the preview responsive. Raw XML contains the complete report.
` : "" }
${renderedResults .map((result, index) => renderResult( result, index === 0 && result.category === "failed", ), ) .join("")}
`; const trxPreview = preview.querySelector(".trx-preview"); rawSources.set(trxPreview, { source, highlightXml }); applyFilters(trxPreview); } function showView(preview, viewName) { if (!preview) return; if (viewName === "raw") { const rawSource = rawSources.get(preview); const code = preview.querySelector("[data-trx-raw-code]"); if (rawSource && code && code.dataset.trxLoaded !== "true") { if (rawSource.source.length > MAX_HIGHLIGHTED_RAW_CHARS) { code.textContent = rawSource.source; const note = preview.querySelector("[data-trx-raw-note]"); if (note) note.hidden = false; } else { code.innerHTML = rawSource.highlightXml(rawSource.source); } code.dataset.trxLoaded = "true"; } } for (const panel of preview.querySelectorAll("[data-trx-panel]")) { panel.hidden = panel.dataset.trxPanel !== viewName; } } function applyFilters(preview) { if (!preview) return; const query = String( preview.querySelector("[data-trx-search-input]")?.value ?? "", ) .trim() .toLocaleLowerCase(); const terms = query.split(/\s+/).filter(Boolean); const category = preview.querySelector('[data-trx-filter][aria-pressed="true"]')?.dataset .trxFilter ?? "all"; const results = [...preview.querySelectorAll("[data-trx-result]")]; let visible = 0; for (const result of results) { const matchesCategory = category === "all" || result.dataset.trxCategory === category; const searchValue = result.dataset.trxSearch ?? ""; const matchesQuery = terms.every((term) => searchValue.includes(term)); result.hidden = !(matchesCategory && matchesQuery); if (!result.hidden) visible += 1; } const total = Number(preview.dataset.trxTotal) || results.length; const rendered = Number(preview.dataset.trxRendered) || results.length; const count = preview.querySelector("[data-trx-result-count]"); if (count) { count.textContent = total > rendered ? `${visible.toLocaleString()} matching in the first ${rendered.toLocaleString()} of ${total.toLocaleString()} results` : `${visible.toLocaleString()} of ${total.toLocaleString()} results`; } const noResults = preview.querySelector("[data-trx-no-results]"); if (noResults) noResults.hidden = visible > 0; } globalThis.TrxPreview = Object.freeze({ applyFilters, parse: parseReport, render, showView, }); })();