diff --git a/frontend/src/__tests__/history.test.ts b/frontend/src/__tests__/history.test.ts index faf017198..9ff19ba8b 100644 --- a/frontend/src/__tests__/history.test.ts +++ b/frontend/src/__tests__/history.test.ts @@ -246,6 +246,50 @@ describe('History Module', () => { expect(list?.innerHTML).toContain('table'); expect(list?.innerHTML).toContain('ec2'); expect(list?.innerHTML).toContain('us-east-1'); + // Both Monthly Cost and Monthly Savings columns must be present (issue #788). + expect(list?.innerHTML).toContain('Monthly Cost'); + expect(list?.innerHTML).toContain('Monthly Savings'); + }); + + test('renders monthly_cost cell with value when present, dash when absent (issue #788)', async () => { + (api.getHistory as jest.Mock).mockResolvedValue({ + summary: {}, + purchases: [ + { + timestamp: '2024-01-15T00:00:00Z', + provider: 'aws', + service: 'ec2', + resource_type: 't3.medium', + region: 'us-east-1', + count: 1, + term: 1, + upfront_cost: 500, + monthly_cost: 42.5, + estimated_savings: 20, + }, + { + timestamp: '2024-01-16T00:00:00Z', + provider: 'aws', + service: 'rds', + resource_type: 'db.m5.large', + region: 'us-east-1', + count: 1, + term: 1, + upfront_cost: 0, + // monthly_cost intentionally absent + estimated_savings: 10, + }, + ] + }); + + await loadHistory(); + + const list = document.getElementById('history-list'); + const html = list?.innerHTML || ''; + // Row with monthly_cost=42.5: formatCurrency mock returns "$42.5". + expect(html).toContain('$42.5'); + // Row without monthly_cost: must render the muted dash, not "$0". + expect(html).toContain('class="muted"'); }); test('shows empty message when no purchases', async () => { diff --git a/frontend/src/__tests__/inventory.test.ts b/frontend/src/__tests__/inventory.test.ts index 052e7a020..a2adc31b8 100644 --- a/frontend/src/__tests__/inventory.test.ts +++ b/frontend/src/__tests__/inventory.test.ts @@ -215,7 +215,7 @@ describe('loadActiveCommitments — fetch + render flow', () => { // (or drops) a column trips the test. const headers = Array.from(list.querySelectorAll('thead th')).map(th => th.textContent); expect(headers).toEqual([ - 'Provider', 'Account', 'Service', 'Resource type', 'Region', 'Count', 'Term', 'Payment', 'Monthly cost', 'Expires', + 'Provider', 'Account', 'Service', 'Resource type', 'Region', 'Count', 'Term', 'Payment', 'Monthly cost', 'Monthly savings', 'Expires', ]); const rows = list.querySelectorAll('tbody tr'); @@ -223,6 +223,12 @@ describe('loadActiveCommitments — fetch + render flow', () => { // Account cell contains the account name AND its monospaced ID. expect(rows[0]!.textContent).toContain('Prod'); expect(rows[0]!.textContent).toContain('ec2'); + + // Monthly Savings column (10th, 0-indexed 9) must render the estimated_savings value. + // makeCommitment defaults estimated_savings to 80.0; formatCurrency mock returns "$80". + const savingsCell = rows[0]!.querySelector('td:nth-child(10)'); + expect(savingsCell).not.toBeNull(); + expect(savingsCell!.textContent).toContain('80'); }); test('renders an empty paragraph when no commitments are returned', async () => { diff --git a/frontend/src/history.ts b/frontend/src/history.ts index 2ab4c8f35..bc2d09d13 100644 --- a/frontend/src/history.ts +++ b/frontend/src/history.ts @@ -623,6 +623,9 @@ function renderHistoryList(purchases: HistoryPurchase[]): void { })(); const execIdAttr = p.purchase_id ? ` data-execution-id="${escapeHtml(p.purchase_id)}"` : ''; const planCellContent = renderActionCell(p); + const monthlyCostCell = p.monthly_cost != null + ? formatCurrency(p.monthly_cost) + : '-'; return ` ${statusCell} @@ -634,6 +637,7 @@ function renderHistoryList(purchases: HistoryPurchase[]): void { ${p.count} ${formatTerm(p.term)} ${formatCurrency(p.upfront_cost)} + ${monthlyCostCell} ${formatCurrency(p.estimated_savings)} ${planCellContent} @@ -654,6 +658,7 @@ function renderHistoryList(purchases: HistoryPurchase[]): void { Count Term Upfront Cost + Monthly Cost Monthly Savings Plan diff --git a/frontend/src/inventory.ts b/frontend/src/inventory.ts index 8ca889a8d..31c3d379a 100644 --- a/frontend/src/inventory.ts +++ b/frontend/src/inventory.ts @@ -69,7 +69,7 @@ export function switchInventorySubSection(name: string): void { const ACTIVE_COMMITMENTS_LIST_ID = 'active-commitments-list'; const ACTIVE_COMMITMENTS_REFRESH_BTN_ID = 'active-commitments-refresh-btn'; -const ACTIVE_COMMITMENTS_COLS = 10; +const ACTIVE_COMMITMENTS_COLS = 11; /** * Fetch and render the active-commitments table. Replaces #active-commitments-list @@ -153,7 +153,7 @@ function renderActiveCommitmentsTable(container: HTMLElement, commitments: api.I const thead = document.createElement('thead'); const headerRow = document.createElement('tr'); - const headers = ['Provider', 'Account', 'Service', 'Resource type', 'Region', 'Count', 'Term', 'Payment', 'Monthly cost', 'Expires']; + const headers = ['Provider', 'Account', 'Service', 'Resource type', 'Region', 'Count', 'Term', 'Payment', 'Monthly cost', 'Monthly savings', 'Expires']; for (const label of headers) { const th = document.createElement('th'); th.textContent = label; @@ -183,6 +183,7 @@ function buildCommitmentRow(c: api.InventoryCommitment): HTMLTableRowElement { appendCell(tr, `${c.term_years}y`); appendCell(tr, c.payment_option ?? ''); appendCell(tr, formatCurrency(c.monthly_cost)); + appendCell(tr, formatCurrency(c.estimated_savings)); appendCell(tr, formatDate(c.end_date)); return tr;