UNPKG

@vizzly-testing/cli

Version:

Visual review platform for UI developers and designers

559 lines (558 loc) 25.3 kB
import { jsx, jsxs, Fragment } from "react/jsx-runtime"; import { renderToString } from "react-dom/server"; const statusConfig = { failed: { label: "Changed", borderClass: "border-l-red-500", badgeClass: "bg-red-500/15 text-red-400 ring-red-500/30", dotClass: "bg-red-500", sectionTitle: "Visual Changes", sectionIcon: "◐" }, new: { label: "New", borderClass: "border-l-blue-500", badgeClass: "bg-blue-500/15 text-blue-400 ring-blue-500/30", dotClass: "bg-blue-500", sectionTitle: "New Screenshots", sectionIcon: "+" }, passed: { label: "Passed", borderClass: "border-l-emerald-500", badgeClass: "bg-emerald-500/15 text-emerald-400 ring-emerald-500/30", dotClass: "bg-emerald-500", sectionTitle: "Passed", sectionIcon: "✓" }, error: { label: "Error", borderClass: "border-l-orange-500", badgeClass: "bg-orange-500/15 text-orange-400 ring-orange-500/30", dotClass: "bg-orange-500", sectionTitle: "Errors", sectionIcon: "!" } }; function StatusBadge({ status }) { let config = statusConfig[status] || statusConfig.error; return /* @__PURE__ */ jsxs( "span", { className: `inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium ring-1 ring-inset ${config.badgeClass}`, children: [ /* @__PURE__ */ jsx("span", { className: `w-1.5 h-1.5 rounded-full ${config.dotClass}` }), config.label ] } ); } function DiffBadge({ percentage }) { if (percentage === void 0 || percentage === null || percentage === 0) return null; return /* @__PURE__ */ jsxs("span", { className: "font-mono text-xs text-red-400/80 tabular-nums", children: [ percentage.toFixed(2), "%" ] }); } function MetaInfo({ properties }) { if (!properties) return null; let { viewport_width, viewport_height, browser } = properties; return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-slate-500", children: [ viewport_width && viewport_height && /* @__PURE__ */ jsxs("span", { className: "font-mono tabular-nums", children: [ viewport_width, "×", viewport_height ] }), browser && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("span", { className: "text-slate-600", children: "·" }), /* @__PURE__ */ jsx("span", { className: "capitalize", children: browser }) ] }) ] }); } function FailedComparison({ comparison, isEven }) { let { name, status, current, baseline, diff, diffPercentage, properties } = comparison; let config = statusConfig[status] || statusConfig.failed; return /* @__PURE__ */ jsxs("details", { className: "group", children: [ /* @__PURE__ */ jsxs( "summary", { className: ` flex items-center gap-4 p-4 cursor-pointer ${isEven ? "bg-slate-800/30" : "bg-slate-800/50"} hover:bg-slate-700/50 border-l-4 ${config.borderClass} rounded-r-lg transition-all duration-150 list-none [&::-webkit-details-marker]:hidden focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900 `, children: [ /* @__PURE__ */ jsxs("div", { className: "relative w-16 h-10 flex-shrink-0 rounded overflow-hidden bg-slate-900", children: [ (current || baseline) && /* @__PURE__ */ jsx( "img", { src: current || baseline, alt: "", className: "w-full h-full object-cover object-top" } ), diff && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-red-500/20 flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "w-2 h-2 bg-red-500 rounded-full animate-pulse" }) }) ] }), /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ jsx("span", { className: "font-medium text-white truncate", children: name }), /* @__PURE__ */ jsx(DiffBadge, { percentage: diffPercentage }) ] }), /* @__PURE__ */ jsx(MetaInfo, { properties }) ] }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [ /* @__PURE__ */ jsx(StatusBadge, { status }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-slate-500 group-hover:text-slate-400 transition-colors", children: [ /* @__PURE__ */ jsx("span", { className: "text-xs hidden sm:inline opacity-0 group-hover:opacity-100 transition-opacity", children: "Details" }), /* @__PURE__ */ jsx( "svg", { className: "w-5 h-5 transition-transform duration-200 group-open:rotate-180", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx( "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" } ) } ) ] }) ] }) ] } ), /* @__PURE__ */ jsxs("div", { className: "mt-1 p-4 bg-slate-800/20 rounded-lg border border-slate-700/30 animate-[fadeIn_150ms_ease-out]", children: [ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-4", children: [ baseline && /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-2", children: [ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-slate-400 uppercase tracking-wider", children: "Baseline" }), /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-600", children: "Expected" }) ] }), /* @__PURE__ */ jsx( "a", { href: baseline, target: "_blank", rel: "noopener noreferrer", className: "block rounded-lg overflow-hidden border border-slate-700/50 hover:border-slate-600 transition-colors", children: /* @__PURE__ */ jsx( "img", { src: baseline, alt: `${name} baseline`, className: "w-full" } ) } ) ] }), current && /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-2", children: [ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-slate-400 uppercase tracking-wider", children: "Current" }), /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-600", children: "Actual" }) ] }), /* @__PURE__ */ jsx( "a", { href: current, target: "_blank", rel: "noopener noreferrer", className: "block rounded-lg overflow-hidden border border-slate-700/50 hover:border-slate-600 transition-colors", children: /* @__PURE__ */ jsx("img", { src: current, alt: `${name} current`, className: "w-full" }) } ) ] }) ] }), diff && /* @__PURE__ */ jsxs("div", { className: "mt-4 pt-4 border-t border-slate-700/30", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-2", children: [ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-red-400 uppercase tracking-wider", children: "Difference" }), diffPercentage > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-red-400/60 font-mono", children: [ diffPercentage.toFixed(2), "% of pixels changed" ] }) ] }), /* @__PURE__ */ jsx( "a", { href: diff, target: "_blank", rel: "noopener noreferrer", className: "block rounded-lg overflow-hidden border border-red-500/30 hover:border-red-500/50 transition-colors max-w-2xl", children: /* @__PURE__ */ jsx("img", { src: diff, alt: `${name} diff`, className: "w-full" }) } ) ] }) ] }) ] }); } function NewComparison({ comparison, isEven }) { let { name, current, baseline, properties } = comparison; let imageSrc = current || baseline; return /* @__PURE__ */ jsxs("details", { className: "group", children: [ /* @__PURE__ */ jsxs( "summary", { className: ` flex items-center gap-4 p-4 cursor-pointer ${isEven ? "bg-slate-800/30" : "bg-slate-800/50"} hover:bg-slate-700/50 border-l-4 border-l-blue-500 rounded-r-lg transition-all duration-150 list-none [&::-webkit-details-marker]:hidden focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900 `, children: [ /* @__PURE__ */ jsx("div", { className: "w-16 h-10 flex-shrink-0 rounded overflow-hidden bg-slate-900", children: imageSrc && /* @__PURE__ */ jsx( "img", { src: imageSrc, alt: "", className: "w-full h-full object-cover object-top" } ) }), /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [ /* @__PURE__ */ jsx("span", { className: "font-medium text-white truncate block", children: name }), /* @__PURE__ */ jsx(MetaInfo, { properties }) ] }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [ /* @__PURE__ */ jsx(StatusBadge, { status: "new" }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-slate-500 group-hover:text-slate-400 transition-colors", children: [ /* @__PURE__ */ jsx("span", { className: "text-xs hidden sm:inline opacity-0 group-hover:opacity-100 transition-opacity", children: "Preview" }), /* @__PURE__ */ jsx( "svg", { className: "w-5 h-5 transition-transform duration-200 group-open:rotate-180", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx( "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" } ) } ) ] }) ] }) ] } ), /* @__PURE__ */ jsxs("div", { className: "mt-1 p-4 bg-slate-800/20 rounded-lg border border-slate-700/30 animate-[fadeIn_150ms_ease-out]", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-2", children: [ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-blue-400 uppercase tracking-wider", children: "New Screenshot" }), /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-600", children: "No baseline to compare" }) ] }), imageSrc && /* @__PURE__ */ jsx( "a", { href: imageSrc, target: "_blank", rel: "noopener noreferrer", className: "block rounded-lg overflow-hidden border border-blue-500/30 hover:border-blue-500/50 transition-colors max-w-2xl", children: /* @__PURE__ */ jsx("img", { src: imageSrc, alt: name, className: "w-full" }) } ) ] }) ] }); } function PassedComparison({ comparison }) { let { name, properties } = comparison; return /* @__PURE__ */ jsxs( "div", { className: ` flex items-center gap-3 px-4 py-2.5 bg-slate-800/20 hover:bg-slate-800/30 border-l-4 border-l-emerald-500/50 rounded-r-lg transition-colors `, children: [ /* @__PURE__ */ jsx("div", { className: "w-5 h-5 rounded-full bg-emerald-500/20 flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx( "svg", { className: "w-3 h-3 text-emerald-400", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsx( "path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" } ) } ) }), /* @__PURE__ */ jsx("span", { className: "flex-1 text-sm text-slate-300 truncate", children: name }), /* @__PURE__ */ jsx(MetaInfo, { properties }) ] } ); } function SectionHeader({ title, count, icon, colorClass }) { if (count === 0) return null; return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-3", children: [ /* @__PURE__ */ jsx("span", { className: `text-lg font-mono ${colorClass}`, children: icon }), /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-slate-300 uppercase tracking-wider", children: title }), /* @__PURE__ */ jsxs("span", { className: "text-sm text-slate-500 font-mono", children: [ "(", count, ")" ] }) ] }); } function SummaryBar({ stats }) { let hasIssues = stats.failed > 0 || stats.new > 0; return /* @__PURE__ */ jsxs( "div", { className: ` flex flex-wrap items-center gap-6 p-4 rounded-xl mb-8 ${hasIssues ? "bg-slate-800/60 border border-slate-700/50" : "bg-emerald-500/10 border border-emerald-500/20"} `, children: [ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3", children: hasIssues ? /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("div", { className: "w-10 h-10 rounded-full bg-amber-500/20 flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "text-amber-400 text-lg", children: "◐" }) }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("div", { className: "text-white font-semibold", children: "Review Required" }), /* @__PURE__ */ jsxs("div", { className: "text-sm text-slate-400", children: [ stats.failed + (stats.new || 0), " items need attention" ] }) ] }) ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("div", { className: "w-10 h-10 rounded-full bg-emerald-500/20 flex items-center justify-center", children: /* @__PURE__ */ jsx( "svg", { className: "w-5 h-5 text-emerald-400", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsx( "path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" } ) } ) }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("div", { className: "text-white font-semibold", children: "All Tests Passed" }), /* @__PURE__ */ jsx("div", { className: "text-sm text-slate-400", children: "No visual changes detected" }) ] }) ] }) }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-6 ml-auto", children: [ /* @__PURE__ */ jsxs("div", { className: "text-center", children: [ /* @__PURE__ */ jsx("div", { className: "text-2xl font-bold text-white tabular-nums", children: stats.total }), /* @__PURE__ */ jsx("div", { className: "text-xs text-slate-500 uppercase tracking-wider", children: "Total" }) ] }), /* @__PURE__ */ jsxs("div", { className: "text-center", children: [ /* @__PURE__ */ jsx("div", { className: "text-2xl font-bold text-emerald-400 tabular-nums", children: stats.passed }), /* @__PURE__ */ jsx("div", { className: "text-xs text-slate-500 uppercase tracking-wider", children: "Passed" }) ] }), stats.failed > 0 && /* @__PURE__ */ jsxs("div", { className: "text-center", children: [ /* @__PURE__ */ jsx("div", { className: "text-2xl font-bold text-red-400 tabular-nums", children: stats.failed }), /* @__PURE__ */ jsx("div", { className: "text-xs text-slate-500 uppercase tracking-wider", children: "Changed" }) ] }), (stats.new || 0) > 0 && /* @__PURE__ */ jsxs("div", { className: "text-center", children: [ /* @__PURE__ */ jsx("div", { className: "text-2xl font-bold text-blue-400 tabular-nums", children: stats.new }), /* @__PURE__ */ jsx("div", { className: "text-xs text-slate-500 uppercase tracking-wider", children: "New" }) ] }) ] }) ] } ); } function formatTimestamp(timestamp) { if (!timestamp) return null; let date = new Date(timestamp); return date.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); } function Header({ timestamp }) { return /* @__PURE__ */ jsx("header", { className: "bg-slate-950/80 backdrop-blur-sm border-b border-slate-800/60 sticky top-0 z-10", children: /* @__PURE__ */ jsxs("div", { className: "max-w-6xl mx-auto px-6 py-4 flex items-center justify-between", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [ /* @__PURE__ */ jsx("div", { className: "w-9 h-9 bg-gradient-to-br from-amber-400 to-amber-600 rounded-lg flex items-center justify-center shadow-lg shadow-amber-500/20", children: /* @__PURE__ */ jsxs( "svg", { className: "w-5 h-5 text-slate-900", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: [ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }), /* @__PURE__ */ jsx("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" }) ] } ) }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx("div", { className: "text-lg font-semibold text-white tracking-tight", children: "Vizzly" }), /* @__PURE__ */ jsx("div", { className: "text-xs text-slate-500", children: "Visual Test Report" }) ] }) ] }), timestamp && /* @__PURE__ */ jsx("div", { className: "text-xs text-slate-500 tabular-nums", children: formatTimestamp(timestamp) }) ] }) }); } function StaticReportView({ reportData }) { if (!reportData || !reportData.comparisons) { return /* @__PURE__ */ jsx("div", { className: "min-h-screen bg-slate-900 text-white flex items-center justify-center", children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [ /* @__PURE__ */ jsx("div", { className: "text-xl font-medium mb-2", children: "No Report Data" }), /* @__PURE__ */ jsx("p", { className: "text-slate-400", children: "No comparison data available for this report." }) ] }) }); } let { comparisons } = reportData; let stats = { total: comparisons.length, passed: comparisons.filter((c) => c.status === "passed").length, failed: comparisons.filter((c) => c.status === "failed").length, new: comparisons.filter((c) => c.status === "new").length, error: comparisons.filter((c) => c.status === "error").length }; let failed = comparisons.filter((c) => c.status === "failed"); let newItems = comparisons.filter((c) => c.status === "new"); let passed = comparisons.filter((c) => c.status === "passed"); let errors = comparisons.filter((c) => c.status === "error"); return /* @__PURE__ */ jsxs("div", { className: "min-h-screen bg-slate-900 text-white", children: [ /* @__PURE__ */ jsx(Header, { timestamp: reportData.timestamp }), /* @__PURE__ */ jsxs("main", { className: "max-w-6xl mx-auto px-6 py-8", children: [ /* @__PURE__ */ jsx(SummaryBar, { stats }), failed.length > 0 && /* @__PURE__ */ jsxs("section", { className: "mb-8", children: [ /* @__PURE__ */ jsx( SectionHeader, { title: "Visual Changes", count: failed.length, icon: "◐", colorClass: "text-red-400" } ), /* @__PURE__ */ jsx("div", { className: "space-y-2", children: failed.map((comparison, index) => /* @__PURE__ */ jsx( FailedComparison, { comparison, isEven: index % 2 === 0 }, comparison.id || comparison.signature || `failed-${index}` )) }) ] }), newItems.length > 0 && /* @__PURE__ */ jsxs("section", { className: "mb-8", children: [ /* @__PURE__ */ jsx( SectionHeader, { title: "New Screenshots", count: newItems.length, icon: "+", colorClass: "text-blue-400" } ), /* @__PURE__ */ jsx("div", { className: "space-y-2", children: newItems.map((comparison, index) => /* @__PURE__ */ jsx( NewComparison, { comparison, isEven: index % 2 === 0 }, comparison.id || comparison.signature || `new-${index}` )) }) ] }), errors.length > 0 && /* @__PURE__ */ jsxs("section", { className: "mb-8", children: [ /* @__PURE__ */ jsx( SectionHeader, { title: "Errors", count: errors.length, icon: "!", colorClass: "text-orange-400" } ), /* @__PURE__ */ jsx("div", { className: "space-y-2", children: errors.map((comparison, index) => /* @__PURE__ */ jsx( FailedComparison, { comparison, isEven: index % 2 === 0 }, comparison.id || comparison.signature || `error-${index}` )) }) ] }), passed.length > 0 && /* @__PURE__ */ jsx("section", { className: "mb-8", children: /* @__PURE__ */ jsxs("details", { className: "group", children: [ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer list-none [&::-webkit-details-marker]:hidden", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-3", children: [ /* @__PURE__ */ jsx("span", { className: "text-lg font-mono text-emerald-400", children: "✓" }), /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-slate-300 uppercase tracking-wider", children: "Passed" }), /* @__PURE__ */ jsxs("span", { className: "text-sm text-slate-500 font-mono", children: [ "(", passed.length, ")" ] }), /* @__PURE__ */ jsx( "svg", { className: "w-4 h-4 text-slate-500 transition-transform group-open:rotate-180 ml-auto", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx( "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" } ) } ) ] }) }), /* @__PURE__ */ jsx("div", { className: "space-y-1", children: passed.map((comparison, index) => /* @__PURE__ */ jsx( PassedComparison, { comparison }, comparison.id || comparison.signature || `passed-${index}` )) }) ] }) }), /* @__PURE__ */ jsx("footer", { className: "mt-12 pt-6 border-t border-slate-800/60 text-center", children: /* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-500", children: [ "Visual regression report generated by", " ", /* @__PURE__ */ jsx( "a", { href: "https://vizzly.dev", className: "text-amber-400 hover:text-amber-300 transition-colors", children: "Vizzly" } ) ] }) }) ] }) ] }); } function renderStaticReport(reportData) { return renderToString(/* @__PURE__ */ jsx(StaticReportView, { reportData })); } export { renderStaticReport };