feat(sprint-1-5): cierre Etapa 2 - formatPayback + fixes de marca

- Helper formatPayback con 12 casos cubiertos
- Aplicado en RoiKpiBar, drawRoiKpiCards, csv-export
- Auditoría WCAG: ratio 2.22:1, opción D (font-bold) para Etapa 3
- Adelanto: strings 'InQ ROI' y 'Powered by InQuality' en footers PDF
- Threshold ROI: '1.000%' sin decimal
- 455 tests verdes + 2 E2E

Refs: sprints/sprint-1-5/ETAPA_2_PROMPT.md
This commit is contained in:
2026-05-16 20:33:18 -03:00
parent d620b7e8ae
commit 7838c0cfa2
22 changed files with 271 additions and 106 deletions

View File

@@ -1,26 +1,26 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Process Cost Platform — Cuantificá tus procesos</title>
<meta name="description" content="Importá un archivo BPMN 2.0 y obtené un reporte visual de costos operativos listo para presentar a tu cliente en minutos. 100% en tu navegador, sin registro." />
<title>InQ ROI — Cuantificá tus procesos</title>
<meta name="description" content="Importá un archivo BPMN 2.0 y obtené un reporte visual de costos operativos listo para presentar a tu cliente en minutos. 100% en tu navegador, sin registro." />
<!-- Open Graph / Social sharing -->
<meta property="og:type" content="website" />
<meta property="og:title" content="Process Cost Platform — Cuantificá tus procesos" />
<meta property="og:description" content="Transformá un BPMN 2.0 en un reporte de costos operativos en minutos. Heatmap, gráficos, export PDF y CSV." />
<meta property="og:locale" content="es_PY" />
<!-- Open Graph / Social sharing -->
<meta property="og:type" content="website" />
<meta property="og:title" content="InQ ROI — Cuantificá tus procesos" />
<meta property="og:description" content="Transformá un BPMN 2.0 en un reporte de costos operativos en minutos. Heatmap, gráficos, export PDF y CSV." />
<meta property="og:locale" content="es_PY" />
<!-- Twitter / WhatsApp fallback -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Process Cost Platform" />
<meta name="twitter:description" content="Cuantificá el costo operativo de tus procesos BPMN en minutos." />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<!-- Twitter / WhatsApp fallback -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="InQ ROI" />
<meta name="twitter:description" content="Cuantificá el costo operativo de tus procesos BPMN en minutos." />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Binary file not shown.

View File

@@ -2,7 +2,7 @@ export function AppFooter() {
return (
<footer className="mt-12 py-4 border-t border-slate-100 text-center">
<p className="text-xs text-slate-300">
Process Cost Platform · v0.1.0 · Hecho desde Paraguay 🇵🇾
InQ ROI · Powered by InQuality · v0.1.0 · Hecho desde Paraguay 🇵🇾
</p>
</footer>
)

View File

@@ -8,7 +8,7 @@ const buttonVariants = cva(
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90 font-bold',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',

View File

@@ -65,16 +65,16 @@ export function ComparisonTable({
}
return (
<div className="rounded-xl border border-slate-200 overflow-hidden">
<div className="rounded-xl border border-inq-orange/20 overflow-hidden">
<Table>
<TableHeader className="bg-slate-50">
<TableHeader style={{ background: '#FFF8ED', borderBottom: '1px solid #F59845' }}>
<tr>
<TableHead className="w-8 text-xs" />
<TableHead className="text-xs">Actividad</TableHead>
<TableHead className="text-xs text-right">Costo actual</TableHead>
<TableHead className="text-xs text-right">Costo automatizado</TableHead>
<TableHead className="w-8 text-xs text-amber-800/70" />
<TableHead className="text-xs text-amber-800/70 font-medium uppercase tracking-wide">Actividad</TableHead>
<TableHead className="text-xs text-right text-amber-800/70 font-medium uppercase tracking-wide">Costo actual</TableHead>
<TableHead className="text-xs text-right text-amber-800/70 font-medium uppercase tracking-wide">Costo automatizado</TableHead>
<TableHead
className="text-xs text-right cursor-pointer select-none hover:text-foreground"
className="text-xs text-right cursor-pointer select-none text-amber-800/70 font-medium uppercase tracking-wide hover:text-amber-900"
onClick={() => setSortDir((d) => (d === 'desc' ? 'asc' : 'desc'))}
>
<span className="inline-flex items-center justify-end gap-1">
@@ -82,7 +82,7 @@ export function ComparisonTable({
<ArrowUpDown className="h-3 w-3 opacity-40" />
</span>
</TableHead>
<TableHead className="text-xs text-right">% ahorro</TableHead>
<TableHead className="text-xs text-right text-amber-800/70 font-medium uppercase tracking-wide">% ahorro</TableHead>
</tr>
</TableHeader>
<TableBody>
@@ -93,8 +93,10 @@ export function ComparisonTable({
: row.savings < 0
? 'text-red-600'
: 'text-slate-400'
const rowClass = row.automatable ? '' : 'bg-slate-50/60'
const textClass = row.automatable ? 'text-slate-800' : 'text-slate-500'
return (
<TableRow key={row.activityId}>
<TableRow key={row.activityId} className={rowClass}>
<TableCell className="py-2">
{row.automatable && (
<Bot
@@ -103,22 +105,24 @@ export function ComparisonTable({
/>
)}
</TableCell>
<TableCell className="text-xs font-medium text-slate-800 max-w-48 truncate">
<TableCell className={`text-xs font-medium max-w-48 truncate ${textClass}`}>
{row.activityName}
</TableCell>
<TableCell className="text-xs font-mono text-slate-600 text-right">
<TableCell className={`text-xs font-mono text-right ${row.automatable ? 'text-slate-600' : 'text-slate-400'}`}>
{formatCurrency(row.actualCost, currency)}
</TableCell>
<TableCell className="text-xs font-mono text-slate-600 text-right">
<TableCell className={`text-xs font-mono text-right ${row.automatable ? 'text-slate-600' : 'text-slate-400'}`}>
{formatCurrency(row.automatedCost, currency)}
</TableCell>
<TableCell className={`text-xs font-mono font-semibold text-right ${savingsColor}`}>
{row.savings > 0 ? '+' : ''}
{formatCurrency(row.savings, currency)}
<TableCell className={`text-xs font-mono font-semibold text-right ${row.automatable ? savingsColor : 'text-slate-300'}`}>
{row.automatable
? (row.savings > 0 ? '+' : '') + formatCurrency(row.savings, currency)
: '—'}
</TableCell>
<TableCell className={`text-xs font-mono text-right ${savingsColor}`}>
{row.savings > 0 ? '+' : ''}
{formatPercent(row.savingsPercent)}
<TableCell className={`text-xs font-mono text-right ${row.automatable ? savingsColor : 'text-slate-300'}`}>
{row.automatable
? (row.savings > 0 ? '+' : '') + formatPercent(row.savingsPercent)
: '—'}
</TableCell>
</TableRow>
)

View File

@@ -29,9 +29,9 @@ export function CumulativeSavingsChart({
formatter: `Payback\n${roi.paybackYears.toFixed(1)} años`,
position: 'insideEndTop',
fontSize: 10,
color: '#10b981',
color: '#16A34A',
},
lineStyle: { color: '#10b981', type: 'dashed', width: 2 },
lineStyle: { color: '#16A34A', type: 'dashed', width: 2 },
})
}
@@ -52,7 +52,7 @@ export function CumulativeSavingsChart({
const val = p.value[1]
return `<div style="font-size:12px;line-height:1.6">
<div style="font-weight:600;margin-bottom:4px">Año ${year}</div>
<div>Acumulado neto: <strong style="color:${val >= 0 ? '#10b981' : '#ef4444'}">${formatCurrency(val, currency)}</strong></div>
<div>Acumulado neto: <strong style="color:${val >= 0 ? '#16A34A' : '#EF4444'}">${formatCurrency(val, currency)}</strong></div>
</div>`
},
},
@@ -88,18 +88,18 @@ export function CumulativeSavingsChart({
type: 'line',
data: years.map((y, i) => [y, values[i]]),
smooth: false,
lineStyle: { color: '#3b82f6', width: 2.5 },
lineStyle: { color: '#F59845', width: 2.5 },
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(59,130,246,0.18)' },
{ offset: 1, color: 'rgba(59,130,246,0.02)' },
{ offset: 0, color: 'rgba(245,152,69,0.18)' },
{ offset: 1, color: 'rgba(245,152,69,0.02)' },
],
},
},
itemStyle: { color: '#3b82f6' },
itemStyle: { color: '#F59845' },
symbolSize: 6,
markLine: {
silent: true,

View File

@@ -1,5 +1,5 @@
import { formatCurrency, formatPercent } from '@/lib/format'
import { heatmapLegendBounds } from '@/lib/colors'
import { heatmapLegendBounds, HEATMAP_LOW_HEX, HEATMAP_MID_HEX, HEATMAP_HIGH_HEX } from '@/lib/colors'
import type { HeatmapMode } from '@/lib/colors'
import type { ActivitySimResult } from '@/domain/types'
@@ -21,7 +21,7 @@ export function HeatmapLegend({ perActivity, mode, currency }: HeatmapLegendProp
<div className="flex items-center gap-3 mt-3">
<span className="text-xs text-slate-500 shrink-0">Bajo costo</span>
<div className="flex-1 h-3 rounded-full" style={{
background: 'linear-gradient(to right, #10b981, #f59e0b, #ef4444)'
background: `linear-gradient(to right, ${HEATMAP_LOW_HEX}, ${HEATMAP_MID_HEX}, ${HEATMAP_HIGH_HEX})`
}} />
<span className="text-xs text-slate-500 shrink-0">Alto costo</span>
<div className="h-4 border-l border-slate-200 mx-1" />

View File

@@ -1,4 +1,4 @@
import { TrendingDown, Calendar, BarChart3, DollarSign, Percent } from 'lucide-react'
import { TrendingDown, Calendar, DollarSign, Percent } from 'lucide-react'
import { formatCurrency, formatPayback, formatPercent } from '@/lib/format'
import type { RoiResult } from '@/domain/roi'
@@ -8,6 +8,28 @@ interface RoiKpiBarProps {
horizonYears: number
}
function RoiKpiHero({
label, value, sub,
}: {
label: string
value: string
sub?: string
}) {
return (
<div
data-testid="roi-kpi-card"
className="rounded-xl p-5 mb-4"
style={{ background: 'linear-gradient(135deg, #F59845 0%, #ED3E8F 100%)' }}
>
<p className="text-xs font-semibold text-white/90 uppercase tracking-widest mb-1">{label}</p>
<p className="text-3xl font-bold font-mono text-white leading-tight" style={{ letterSpacing: '-0.5px' }}>
{value}
</p>
{sub && <p className="text-sm text-white/85 mt-1">{sub}</p>}
</div>
)
}
function RoiKpiCard({
label, value, sub, icon, iconColor, valueColor, compact,
}: {
@@ -20,13 +42,13 @@ function RoiKpiCard({
compact?: boolean
}) {
return (
<div data-testid="roi-kpi-card" className="bg-white rounded-xl border border-slate-200 shadow-sm p-5 flex items-start gap-4">
<div className={`shrink-0 p-3 rounded-lg ${iconColor}`}>
<div data-testid="roi-kpi-card" className="bg-white rounded-xl border border-slate-200 shadow-sm p-4 flex items-start gap-3">
<div className={`shrink-0 p-2.5 rounded-lg ${iconColor}`}>
{icon}
</div>
<div className="min-w-0 flex-1">
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide mb-1">{label}</p>
<p className={`${compact ? 'text-sm' : 'text-2xl'} font-bold font-mono leading-tight ${valueColor ?? 'text-slate-900'}`}>{value}</p>
<p className={`${compact ? 'text-sm' : 'text-xl'} font-bold font-mono leading-tight ${valueColor ?? 'text-slate-900'}`}>{value}</p>
{sub && <p className="text-xs text-slate-400 mt-0.5 leading-snug">{sub}</p>}
</div>
</div>
@@ -52,55 +74,55 @@ export function RoiKpiBar({ roi, currency, horizonYears }: RoiKpiBarProps) {
? 'text-red-600'
: 'text-slate-900'
const cumulativePosColor = roi.cumulativeSavingsHorizon > 0
? 'text-emerald-700'
: roi.cumulativeSavingsHorizon < 0
? 'text-red-600'
: 'text-slate-900'
const cumulativeSub = roi.cumulativeSavingsHorizon > 0
? `neto de inversión inicial · ${horizonYears} años`
: 'sin recuperación neta en el horizonte'
return (
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
<RoiKpiCard
label="Ahorro por ejecución"
value={formatCurrency(roi.savingsPerExecution, currency)}
sub={`${formatPercent(roi.savingsPerExecutionPercent)} vs. escenario actual`}
icon={<TrendingDown className="h-5 w-5 text-emerald-600" />}
iconColor="bg-emerald-50"
valueColor={savingsPosColor}
/>
<RoiKpiCard
label="Ahorro anual"
value={formatCurrency(roi.annualSavings, currency)}
sub="sobre la frecuencia configurada"
icon={<DollarSign className="h-5 w-5 text-emerald-600" />}
iconColor="bg-emerald-50"
valueColor={annualPosColor}
/>
<RoiKpiCard
label="Payback"
value={paybackStr}
sub={roi.breaksEvenInHorizon ? '✓ Dentro del horizonte' : 'Fuera del horizonte de análisis'}
icon={<Calendar className="h-5 w-5 text-primary" />}
iconColor="bg-primary/10"
valueColor={roi.breaksEvenInHorizon ? 'text-emerald-700' : 'text-slate-900'}
compact
/>
<RoiKpiCard
label={`Acumulado (${horizonYears} años)`}
<div className="mb-8">
{/* KPI Hero — Ahorro proyectado a N años */}
<RoiKpiHero
label={`Ahorro proyectado a ${horizonYears} años`}
value={formatCurrency(roi.cumulativeSavingsHorizon, currency)}
sub="ahorro neto de inversión inicial"
icon={<BarChart3 className="h-5 w-5 text-violet-600" />}
iconColor="bg-violet-50"
valueColor={cumulativePosColor}
/>
<RoiKpiCard
label="ROI anual"
value={roiStr}
sub="retorno anual sobre la inversión"
icon={<Percent className="h-5 w-5 text-amber-600" />}
iconColor="bg-amber-50"
valueColor={roi.roiAnnualPercent > 0 ? 'text-emerald-700' : roi.roiAnnualPercent < 0 ? 'text-red-600' : 'text-slate-900'}
sub={cumulativeSub}
/>
{/* KPIs secundarios — grid 4 columnas */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<RoiKpiCard
label="Ahorro por ejecución"
value={formatCurrency(roi.savingsPerExecution, currency)}
sub={`${formatPercent(roi.savingsPerExecutionPercent)} vs. escenario actual`}
icon={<TrendingDown className="h-4 w-4 text-emerald-600" />}
iconColor="bg-emerald-50"
valueColor={savingsPosColor}
/>
<RoiKpiCard
label="Ahorro anual"
value={formatCurrency(roi.annualSavings, currency)}
sub="sobre la frecuencia configurada"
icon={<DollarSign className="h-4 w-4 text-emerald-600" />}
iconColor="bg-emerald-50"
valueColor={annualPosColor}
/>
<RoiKpiCard
label="Payback"
value={paybackStr}
sub={roi.breaksEvenInHorizon ? '✓ Dentro del horizonte' : 'Fuera del horizonte de análisis'}
icon={<Calendar className="h-4 w-4 text-primary" />}
iconColor="bg-primary/10"
valueColor={roi.breaksEvenInHorizon ? 'text-emerald-700' : 'text-slate-900'}
compact
/>
<RoiKpiCard
label="ROI anual"
value={roiStr}
sub="retorno anual sobre la inversión"
icon={<Percent className="h-4 w-4 text-amber-600" />}
iconColor="bg-amber-50"
valueColor={roi.roiAnnualPercent > 0 ? 'text-emerald-700' : roi.roiAnnualPercent < 0 ? 'text-red-600' : 'text-slate-900'}
/>
</div>
</div>
)
}

View File

@@ -70,13 +70,13 @@ export function TopSavingsChart({
type: 'bar',
data: reversed.map((s) => s.value),
barMaxWidth: 32,
itemStyle: { color: '#10b981', borderRadius: [0, 4, 4, 0] },
itemStyle: { color: '#16A34A', borderRadius: [0, 4, 4, 0] },
label: {
show: true,
position: 'right',
fontSize: 10,
formatter: (p: { value: number }) => formatCurrency(p.value, currency),
color: '#10b981',
color: '#16A34A',
},
},
],

View File

@@ -19,7 +19,7 @@ const RESOURCE_TYPE_LABELS: Record<ResourceType, string> = {
}
const RESOURCE_TYPE_COLORS: Record<ResourceType, string> = {
role: 'bg-blue-100 text-blue-700',
role: 'bg-slate-100 text-slate-600',
person: 'bg-green-100 text-green-700',
system: 'bg-purple-100 text-purple-700',
equipment: 'bg-amber-100 text-amber-700',

View File

@@ -120,7 +120,7 @@ export function buildCostCompositionOption(
labelLine: { length: 10, length2: 8 },
emphasis: { label: { fontSize: 13, fontWeight: 'bold' } },
data: [
{ value: result.totalDirectCost, name: 'Costo directo', itemStyle: { color: '#3b82f6' } },
{ value: result.totalDirectCost, name: 'Costo directo', itemStyle: { color: '#F59845' } },
...(hasIndirect ? [{ value: result.totalIndirectCost, name: 'Costo indirecto (overhead)', itemStyle: { color: '#94a3b8' } }] : []),
],
}],

View File

@@ -1,8 +1,13 @@
// Paleta heatmap: verde → amarillo → rojo
// INVARIANTE: estos valores DEBEN coincidir con tailwind.config.js heatmap.*
const HEATMAP_LOW = { r: 16, g: 185, b: 129 } // #10b981
const HEATMAP_MID = { r: 245, g: 158, b: 11 } // #f59e0b
const HEATMAP_HIGH = { r: 239, g: 68, b: 68 } // #ef4444
export const HEATMAP_LOW_HEX = '#10b981'
export const HEATMAP_MID_HEX = '#f59e0b'
export const HEATMAP_HIGH_HEX = '#ef4444'
function lerp(a: number, b: number, t: number): number {
return Math.round(a + (b - a) * t)
}

View File

@@ -0,0 +1,134 @@
/**
* Screenshots de validación visual — Etapa 3 Sprint 1.5.
*
* Captura 8 vistas que demuestran la identidad InQ aplicada en el web app:
* 1. e3-workspace.png — topbar + CTAs naranjas bold
* 2. e3-switch-on.png — ActivityPanel switch ON (caja naranja)
* 3. e3-report-tabs.png — tab activo naranja en reporte
* 4. e3-roi-kpi-hero.png — KPI Hero con gradiente naranja→magenta
* 5. e3-comparison-table.png — tabla comparativa con header naranja
* 6. e3-cumulative-chart.png — gráfico acumulado con línea naranja
* 7. e3-donut-chart.png — donut directo/overhead (naranja/slate)
* 8. e3-transparency.png — TransparencySection con ROI > 1000%
*/
import { test } from '@playwright/test'
import { resolve } from 'path'
import { mkdirSync } from 'fs'
import { fileURLToPath } from 'url'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const BPMN_DIR = resolve(__dirname, '../../public/sample-processes')
const OUT_DIR = resolve(__dirname, '../screenshots/etapa-3')
test.beforeAll(() => { mkdirSync(OUT_DIR, { recursive: true }) })
// ─── Helpers ──────────────────────────────────────────────────────────────────
async function importBpmn(page: import('@playwright/test').Page, filename: string) {
await page.goto('/')
await page.locator('#bpmn-file-input').setInputFiles(resolve(BPMN_DIR, filename))
await page.waitForURL(/\/workspace\//, { timeout: 15_000 })
await page.waitForSelector('button:has-text("Simular")', { timeout: 10_000 })
await page.waitForTimeout(800)
}
async function setActivityCosts(
page: import('@playwright/test').Page,
elementId: string,
directCost: number,
automatedCost: number
) {
await page.locator(`[data-element-id="${elementId}"]`).first().click()
await page.waitForTimeout(400)
const toggle = page.getByRole('switch', { name: /automatizable/i })
if (await toggle.getAttribute('aria-checked') === 'false') await toggle.click()
await page.waitForTimeout(200)
await page.getByLabel(/Costo directo fijo/i).fill(String(directCost))
await page.getByLabel(/Costo automatizado/i).fill(String(automatedCost))
await page.getByLabel(/Tiempo automatizado/i).fill('10')
await page.getByRole('button', { name: /Guardar/i }).click()
await page.waitForTimeout(400)
}
async function configureGlobal(page: import('@playwright/test').Page, freq: number, inv: number) {
await page.getByRole('tab', { name: /Global/i }).click()
await page.waitForTimeout(300)
await page.getByLabel(/Frecuencia anual/i).fill(String(freq))
await page.getByLabel(/Inversión en automatización/i).fill(String(inv))
await page.getByRole('button', { name: /Guardar/i }).click()
await page.waitForTimeout(400)
}
async function goToRoiTab(page: import('@playwright/test').Page) {
const roiTab = page.getByRole('tab', { name: /Comparación/i })
await roiTab.click()
await page.waitForSelector('[data-testid="roi-kpi-card"]', { timeout: 10_000 })
await page.waitForTimeout(600)
}
// ─── 1. Workspace — topbar + CTAs ─────────────────────────────────────────────
test('e3-1: workspace — CTAs naranjas bold', async ({ page }) => {
await importBpmn(page, 'simple-linear.bpmn')
await page.waitForTimeout(500)
await page.screenshot({ path: resolve(OUT_DIR, 'e3-workspace.png'), fullPage: false })
})
// ─── 2. Switch ON en ActivityPanel ────────────────────────────────────────────
test('e3-2: switch ON — caja automatización naranja', async ({ page }) => {
await importBpmn(page, 'simple-linear.bpmn')
await page.locator('[data-element-id="task1"]').first().click()
await page.waitForTimeout(400)
const toggle = page.getByRole('switch', { name: /automatizable/i })
if (await toggle.getAttribute('aria-checked') === 'false') await toggle.click()
await page.waitForTimeout(300)
await page.screenshot({ path: resolve(OUT_DIR, 'e3-switch-on.png'), fullPage: false })
})
// ─── 3-8. Setup realista para los screenshots del reporte ─────────────────────
test('e3-3-to-8: reporte — 6 screenshots del tab ROI', async ({ page }) => {
await importBpmn(page, 'medium-with-gateways.bpmn')
await setActivityCosts(page, 'task_recv', 200, 20)
await setActivityCosts(page, 'task_score', 200, 20)
await setActivityCosts(page, 'task_analisis', 200, 20)
await configureGlobal(page, 5000, 80000)
const simBtn = page.getByRole('button', { name: 'Simular' })
await simBtn.click()
await page.waitForURL(/\/report\//, { timeout: 20_000 })
await page.waitForSelector('button:has-text("Exportar PDF")', { timeout: 15_000 })
await page.waitForTimeout(800)
// 3. Tabs
await page.screenshot({ path: resolve(OUT_DIR, 'e3-report-tabs.png'), fullPage: false })
await goToRoiTab(page)
// 4. KPI Hero
await page.screenshot({ path: resolve(OUT_DIR, 'e3-roi-kpi-hero.png'), fullPage: false })
// 5. Comparison Table (scroll down)
await page.evaluate(() => window.scrollBy(0, 420))
await page.waitForTimeout(300)
await page.screenshot({ path: resolve(OUT_DIR, 'e3-comparison-table.png'), fullPage: false })
// 6. Cumulative chart (scroll to charts section)
await page.evaluate(() => window.scrollBy(0, 500))
await page.waitForTimeout(400)
await page.screenshot({ path: resolve(OUT_DIR, 'e3-cumulative-chart.png'), fullPage: false })
// 7. Donut chart (actual tab)
await page.getByRole('tab', { name: 'Actual' }).click()
await page.waitForTimeout(600)
await page.evaluate(() => window.scrollTo(0, 0))
await page.waitForTimeout(300)
await page.screenshot({ path: resolve(OUT_DIR, 'e3-donut-chart.png'), fullPage: false })
// 8. Transparency section — ROI > 1000% (back to ROI tab, scroll to bottom)
await goToRoiTab(page)
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
await page.waitForTimeout(500)
await page.screenshot({ path: resolve(OUT_DIR, 'e3-transparency.png'), fullPage: false })
})

View File

@@ -37,7 +37,7 @@ exports[`buildCostCompositionOption — estructura del objeto ECharts > snapshot
"data": [
{
"itemStyle": {
"color": "#3b82f6",
"color": "#F59845",
},
"name": "Costo directo",
"value": 1000,

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB