diff --git a/frontend/src/__tests__/dashboard.test.ts b/frontend/src/__tests__/dashboard.test.ts index 7b35dd964..b8b4775b6 100644 --- a/frontend/src/__tests__/dashboard.test.ts +++ b/frontend/src/__tests__/dashboard.test.ts @@ -575,6 +575,53 @@ describe('Dashboard Module', () => { expect(savingsCard?.textContent).not.toContain('$700'); }); + // #978: Current Coverage KPI must render with exactly 1 decimal place so it + // cannot overflow the tile box at narrower screen widths. + test('#978: Current Coverage KPI rounds to 1 decimal place', async () => { + (api.getDashboardSummary as jest.Mock).mockResolvedValue({ + potential_monthly_savings: 0, + total_recommendations: 3, + active_commitments: 2, + committed_monthly: 100, + // A long-fractional value that previously overflowed the tile box. + current_coverage: 72.3456789012, + target_coverage: 80, + ytd_savings: 0, + by_service: {} + }); + (api.getUpcomingPurchases as jest.Mock).mockResolvedValue({ purchases: [] }); + + await loadDashboard(); + + const coverageTile = document.querySelector('[data-kpi="coverage"]'); + const valueEl = coverageTile?.querySelector('.kpi-tile-value'); + // Must display exactly 1 decimal place (e.g. "72.3%"), not the raw float. + expect(valueEl?.textContent).toBe('72.3%'); + // The raw long-decimal form must not appear. + expect(valueEl?.textContent).not.toContain('72.3456'); + }); + + // Integer coverage values must still format cleanly with one decimal place. + test('#978: integer Current Coverage KPI renders with one decimal place', async () => { + (api.getDashboardSummary as jest.Mock).mockResolvedValue({ + potential_monthly_savings: 0, + total_recommendations: 1, + active_commitments: 1, + committed_monthly: 50, + current_coverage: 70, + target_coverage: 80, + ytd_savings: 0, + by_service: {} + }); + (api.getUpcomingPurchases as jest.Mock).mockResolvedValue({ purchases: [] }); + + await loadDashboard(); + + const coverageTile = document.querySelector('[data-kpi="coverage"]'); + const valueEl = coverageTile?.querySelector('.kpi-tile-value'); + expect(valueEl?.textContent).toBe('70.0%'); + }); + // #293: If getRecommendations rejects, other cards still render. test('failure-isolation: rec fetch failure leaves other cards rendered (#293)', async () => { (api.getRecommendations as jest.Mock).mockRejectedValue(new Error('Network error')); diff --git a/frontend/src/dashboard.ts b/frontend/src/dashboard.ts index e3bab9fc7..0fd463bf6 100644 --- a/frontend/src/dashboard.ts +++ b/frontend/src/dashboard.ts @@ -268,6 +268,8 @@ function renderDashboardSummary(data: DashboardSummary, recs: readonly LocalReco // When no recommendations and no commitments exist, "100% coverage" is // misleading — nothing is being tracked. Show a dash instead. const nothingTracked = !data.total_recommendations && !data.active_commitments; + // #978: round to 1 decimal place to match sibling coverage formatting (inventory.ts) + // and prevent overflow at narrower screen widths. const coverageValue = nothingTracked ? '—' : `${(data.current_coverage || 0).toFixed(1)}%`; const coverageDetail = nothingTracked ? 'No services tracked' : `Target: ${(data.target_coverage || 80).toFixed(1)}%`;