Skip to content

Commit 3e53a66

Browse files
authored
fix(home): round Current Coverage KPI to 1 decimal place (#978) (#980)
Raw float values (e.g. 72.3456789012%) caused the KPI tile to overflow its box at narrower screen widths. Apply .toFixed(1) to match the coverage formatting used throughout the app (inventory.ts overall_coverage_pct, coverage_pct, and all other percentage KPIs). Only display formatting is changed; the underlying data is untouched. Adds two regression tests asserting the bounded precision. Closes #978
1 parent 7485a75 commit 3e53a66

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

‎frontend/src/__tests__/dashboard.test.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,53 @@ describe('Dashboard Module', () => {
575575
expect(savingsCard?.textContent).not.toContain('$700');
576576
});
577577

578+
// #978: Current Coverage KPI must render with exactly 1 decimal place so it
579+
// cannot overflow the tile box at narrower screen widths.
580+
test('#978: Current Coverage KPI rounds to 1 decimal place', async () => {
581+
(api.getDashboardSummary as jest.Mock).mockResolvedValue({
582+
potential_monthly_savings: 0,
583+
total_recommendations: 3,
584+
active_commitments: 2,
585+
committed_monthly: 100,
586+
// A long-fractional value that previously overflowed the tile box.
587+
current_coverage: 72.3456789012,
588+
target_coverage: 80,
589+
ytd_savings: 0,
590+
by_service: {}
591+
});
592+
(api.getUpcomingPurchases as jest.Mock).mockResolvedValue({ purchases: [] });
593+
594+
await loadDashboard();
595+
596+
const coverageTile = document.querySelector('[data-kpi="coverage"]');
597+
const valueEl = coverageTile?.querySelector('.kpi-tile-value');
598+
// Must display exactly 1 decimal place (e.g. "72.3%"), not the raw float.
599+
expect(valueEl?.textContent).toBe('72.3%');
600+
// The raw long-decimal form must not appear.
601+
expect(valueEl?.textContent).not.toContain('72.3456');
602+
});
603+
604+
// Integer coverage values must still format cleanly with one decimal place.
605+
test('#978: integer Current Coverage KPI renders with one decimal place', async () => {
606+
(api.getDashboardSummary as jest.Mock).mockResolvedValue({
607+
potential_monthly_savings: 0,
608+
total_recommendations: 1,
609+
active_commitments: 1,
610+
committed_monthly: 50,
611+
current_coverage: 70,
612+
target_coverage: 80,
613+
ytd_savings: 0,
614+
by_service: {}
615+
});
616+
(api.getUpcomingPurchases as jest.Mock).mockResolvedValue({ purchases: [] });
617+
618+
await loadDashboard();
619+
620+
const coverageTile = document.querySelector('[data-kpi="coverage"]');
621+
const valueEl = coverageTile?.querySelector('.kpi-tile-value');
622+
expect(valueEl?.textContent).toBe('70.0%');
623+
});
624+
578625
// #293: If getRecommendations rejects, other cards still render.
579626
test('failure-isolation: rec fetch failure leaves other cards rendered (#293)', async () => {
580627
(api.getRecommendations as jest.Mock).mockRejectedValue(new Error('Network error'));

‎frontend/src/dashboard.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ function renderDashboardSummary(data: DashboardSummary, recs: readonly LocalReco
268268
// When no recommendations and no commitments exist, "100% coverage" is
269269
// misleading — nothing is being tracked. Show a dash instead.
270270
const nothingTracked = !data.total_recommendations && !data.active_commitments;
271+
// #978: round to 1 decimal place to match sibling coverage formatting (inventory.ts)
272+
// and prevent overflow at narrower screen widths.
271273
const coverageValue = nothingTracked ? '—' : `${(data.current_coverage || 0).toFixed(1)}%`;
272274
const coverageDetail = nothingTracked ? 'No services tracked' : `Target: ${(data.target_coverage || 80).toFixed(1)}%`;
273275

0 commit comments

Comments
 (0)